@zq-silk/yui 2.3.0 → 2.4.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.
Files changed (183) hide show
  1. package/README.md +15 -1
  2. package/dist/artifacts/artifactCapability.js +39 -10
  3. package/dist/artifacts/imageMetadata.js +120 -0
  4. package/dist/artifacts/managedGit.js +7 -4
  5. package/dist/artifacts/taskArtifactRepository.js +6 -0
  6. package/dist/cli/commandCatalog.js +17 -8
  7. package/dist/cli/invocationAuthority.js +1 -1
  8. package/dist/commands/executionAuditCommands.js +5 -0
  9. package/dist/commands/globalRoleCommands.js +41 -3
  10. package/dist/commands/projectCommands.js +89 -94
  11. package/dist/commands/roleSkillValidation.js +4 -8
  12. package/dist/commands/taskCommands.js +25 -2
  13. package/dist/commands/taskUsageCommand.js +38 -0
  14. package/dist/context/roleSessionContext.js +18 -2
  15. package/dist/context/runContextPack.js +5 -1
  16. package/dist/context/sessionBootstrapManifest.js +10 -1
  17. package/dist/context/skillCatalog.js +108 -0
  18. package/dist/context/skillPackage.js +161 -0
  19. package/dist/context/taskContext.js +3 -2
  20. package/dist/context/taskSearch.js +127 -0
  21. package/dist/controlPlaneCli.js +25 -20
  22. package/dist/controller/fileSchedulerStoreAdapter.js +32 -6
  23. package/dist/controller/jobControl.js +2 -0
  24. package/dist/controller/nativeHostInput.js +37 -0
  25. package/dist/controller/runtime.js +93 -2
  26. package/dist/controller/runtimeEventInbox.js +2 -2
  27. package/dist/controller/runtimeEventProcessor.js +3 -3
  28. package/dist/doctor/doctor.js +10 -0
  29. package/dist/execution/reviewMainRun.js +1 -0
  30. package/dist/execution/workItemMainRun.js +1 -0
  31. package/dist/executor/codexConfigConflict.js +6 -4
  32. package/dist/executor/effectiveLaunch.js +24 -1
  33. package/dist/executor/fileRoleLaunchPlanner.js +13 -5
  34. package/dist/integration/gitIntegrationService.js +7 -12
  35. package/dist/integration/integrationSourceApplication.js +4 -2
  36. package/dist/job/httpPreview.js +61 -0
  37. package/dist/job/jobRunner.js +12 -2
  38. package/dist/kernel/builtinCapabilities.js +3 -1
  39. package/dist/message/message.js +15 -1
  40. package/dist/output/rolePresentation.js +4 -1
  41. package/dist/repository/executionLaneGitSnapshot.js +2 -1
  42. package/dist/repository/gitCompatibility.js +62 -0
  43. package/dist/repository/gitWorkspace.js +48 -71
  44. package/dist/repository/gitWorktreeInventory.js +121 -0
  45. package/dist/repository/project.js +74 -25
  46. package/dist/resources/resourceDiscovery.js +2 -2
  47. package/dist/resources/resourceGc.js +7 -4
  48. package/dist/resources/resourceRegistrar.js +2 -1
  49. package/dist/runtime/agentEndpoint.js +8 -0
  50. package/dist/runtime/agentHost.js +170 -3
  51. package/dist/runtime/codexAppServerRuntime.js +1 -1
  52. package/dist/runtime/codexNativeAccess.js +175 -0
  53. package/dist/runtime/controlledCodexTui.js +116 -0
  54. package/dist/runtime/publicReply.js +23 -0
  55. package/dist/runtime/runtimeObservation.js +48 -0
  56. package/dist/runtime/structuredProviderHost.js +16 -7
  57. package/dist/runtime/taskCostMetrics.js +133 -0
  58. package/dist/runtime/taskUsageMetrics.js +11 -2
  59. package/dist/runtime/taskUsageQuery.js +38 -0
  60. package/dist/scheduler/taskExecutionProjection.js +2 -0
  61. package/dist/scheduler/taskObservabilityProjection.js +9 -1
  62. package/dist/storage/homeLayout.js +13 -0
  63. package/dist/storage/sqliteSchema.js +44 -0
  64. package/dist/storage/sqliteStore.js +53 -0
  65. package/dist/storage/storageVersions.js +1 -1
  66. package/dist/storage/storeRpc.js +2 -0
  67. package/dist/storage/taskSearch.js +35 -0
  68. package/dist/storage/upgrade/projectKnowledgeMigration.js +52 -0
  69. package/dist/task/ordinaryArchive.js +42 -0
  70. package/dist/tmux/tmuxManager.js +8 -8
  71. package/dist/web/assets/assetManifest.js +11 -51
  72. package/dist/web/assets/client/app/catalog.js +215 -0
  73. package/dist/web/assets/client/app/main.js +148 -0
  74. package/dist/web/assets/client/app/selection.js +139 -0
  75. package/dist/web/assets/client/app/settings.js +326 -0
  76. package/dist/web/assets/client/app/taskView.js +280 -0
  77. package/dist/web/assets/client/domain/context.js +96 -0
  78. package/dist/web/assets/client/domain/records.js +126 -0
  79. package/dist/web/assets/client/domain/runs.js +119 -0
  80. package/dist/web/assets/client/domain/taskForms.js +197 -0
  81. package/dist/web/assets/client/domain/vocab.js +69 -0
  82. package/dist/web/assets/client/domain/work.js +116 -0
  83. package/dist/web/assets/client/i18n/en.js +900 -0
  84. package/dist/web/assets/client/i18n/zh-CN.js +898 -0
  85. package/dist/web/assets/client/index.js +114 -0
  86. package/dist/web/assets/client/layout/resizer.js +37 -0
  87. package/dist/web/assets/client/layout/router.js +35 -0
  88. package/dist/web/assets/client/layout/shortcuts.js +45 -0
  89. package/dist/web/assets/client/layout/sizing.js +95 -0
  90. package/dist/web/assets/client/layout/workspace.js +247 -0
  91. package/dist/web/assets/client/{api.js → lib/api.js} +20 -4
  92. package/dist/web/assets/client/{dom.js → lib/dom.js} +24 -12
  93. package/dist/web/assets/client/{format.js → lib/format.js} +7 -0
  94. package/dist/web/assets/client/lib/i18n.js +54 -0
  95. package/dist/web/assets/client/lib/markdown.js +167 -0
  96. package/dist/web/assets/client/lib/prefs.js +28 -0
  97. package/dist/web/assets/client/lib/theme.js +68 -0
  98. package/dist/web/assets/client/ui/containers.js +101 -0
  99. package/dist/web/assets/client/ui/controls.js +51 -0
  100. package/dist/web/assets/client/ui/forms.js +48 -0
  101. package/dist/web/assets/client/ui/metrics.js +137 -0
  102. package/dist/web/assets/client/ui/primitives.js +92 -0
  103. package/dist/web/assets/client/ui/text.js +33 -0
  104. package/dist/web/assets/client/ui/toast.js +12 -0
  105. package/dist/web/assets/client/views/bodySearch.js +172 -0
  106. package/dist/web/assets/client/views/createTask.js +85 -0
  107. package/dist/web/assets/client/views/dock/conversation.js +612 -0
  108. package/dist/web/assets/client/views/dock/discussion.js +173 -0
  109. package/dist/web/assets/client/views/dock/preview.js +129 -0
  110. package/dist/web/assets/client/views/dock/terminal.js +199 -0
  111. package/dist/web/assets/client/views/globalInput.js +67 -0
  112. package/dist/web/assets/client/{overview.js → views/overview.js} +61 -51
  113. package/dist/web/assets/client/{sidebar.js → views/sidebar.js} +56 -62
  114. package/dist/web/assets/client/views/skills.js +152 -0
  115. package/dist/web/assets/client/views/task/actions.js +36 -0
  116. package/dist/web/assets/client/views/task/delivery.js +185 -0
  117. package/dist/web/assets/client/views/task/deliveryResults.js +159 -0
  118. package/dist/web/assets/client/views/task/files.js +272 -0
  119. package/dist/web/assets/client/views/task/observation.js +153 -0
  120. package/dist/web/assets/client/views/task/overview.js +161 -0
  121. package/dist/web/assets/client/views/task/page.js +131 -0
  122. package/dist/web/assets/client/views/task/records.js +163 -0
  123. package/dist/web/assets/client/views/task/runtime.js +130 -0
  124. package/dist/web/assets/client/views/task/timeline.js +190 -0
  125. package/dist/web/assets/shared/geometry.js +19 -0
  126. package/dist/web/assets/shared/taskTabs.js +8 -0
  127. package/dist/web/assets/shared/themes.js +93 -0
  128. package/dist/web/assets/shell/dialogs.js +63 -0
  129. package/dist/web/assets/shell/dock.js +40 -0
  130. package/dist/web/assets/shell/index.js +40 -0
  131. package/dist/web/assets/shell/settings.js +34 -0
  132. package/dist/web/assets/shell/sidebar.js +45 -0
  133. package/dist/web/assets/styles/index.js +41 -0
  134. package/dist/web/assets/styles/{responsive.js → layout/responsive.js} +10 -10
  135. package/dist/web/assets/styles/{layout.js → layout/workspace.js} +17 -24
  136. package/dist/web/assets/styles/markdown.js +22 -3
  137. package/dist/web/assets/styles/tokens.js +10 -63
  138. package/dist/web/assets/styles/ui/controls.js +76 -0
  139. package/dist/web/assets/styles/ui/forms.js +46 -0
  140. package/dist/web/assets/styles/ui/surfaces.js +122 -0
  141. package/dist/web/assets/styles/views/delivery.js +29 -0
  142. package/dist/web/assets/styles/views/dock.js +71 -0
  143. package/dist/web/assets/styles/views/page.js +32 -0
  144. package/dist/web/assets/styles/views/records.js +70 -0
  145. package/dist/web/assets/styles/views/settings.js +26 -0
  146. package/dist/web/assets/styles/views/sidebar.js +74 -0
  147. package/dist/web/assets/styles/views/task.js +80 -0
  148. package/dist/web/assets/styles/views/terminal.js +38 -0
  149. package/dist/web/taskPreviews.js +205 -0
  150. package/dist/web/tmuxWebTerminal.js +16 -9
  151. package/dist/web/webConversation.js +373 -0
  152. package/dist/web/webMaterials.js +155 -0
  153. package/dist/web/webServer.js +295 -18
  154. package/dist/web/webSettings.js +331 -0
  155. package/dist/web/webSkills.js +86 -0
  156. package/dist/web/webSnapshot.js +40 -8
  157. package/dist/web/webTaskSurface.js +106 -8
  158. package/dist/workspace/gitChangeSetCapture.js +3 -1
  159. package/docs/managed-turn-and-session-runtime.md +9 -0
  160. package/docs/managed-turn-and-session-runtime.zh-CN.md +7 -0
  161. package/docs/release-workflow.md +28 -0
  162. package/docs/release-workflow.zh-CN.md +21 -0
  163. package/docs/storage-baseline.md +38 -1
  164. package/docs/storage-baseline.zh-CN.md +22 -0
  165. package/docs/task-discovery.md +28 -0
  166. package/docs/testing/verification-levels.md +2 -2
  167. package/i18n/README.zh-CN.md +1 -1
  168. package/package.json +1 -1
  169. package/dist/web/assets/client/app.js +0 -808
  170. package/dist/web/assets/client/components.js +0 -477
  171. package/dist/web/assets/client/detail.js +0 -379
  172. package/dist/web/assets/client/dock.js +0 -309
  173. package/dist/web/assets/client/evidence.js +0 -364
  174. package/dist/web/assets/client/forms.js +0 -230
  175. package/dist/web/assets/client/i18n.js +0 -1277
  176. package/dist/web/assets/client/markdown.js +0 -114
  177. package/dist/web/assets/client/records.js +0 -198
  178. package/dist/web/assets/client/sections.js +0 -349
  179. package/dist/web/assets/client/theme.js +0 -71
  180. package/dist/web/assets/shell.js +0 -161
  181. package/dist/web/assets/styles/components.js +0 -216
  182. package/dist/web/assets/styles/views.js +0 -309
  183. /package/dist/web/assets/{icons.js → shared/icons.js} +0 -0
@@ -1,3 +1,4 @@
1
+ import { projectTaskCosts } from "./taskCostMetrics.js";
1
2
  import { builtinDriverIdForAdapter } from "./builtinAgentDrivers.js";
2
3
  import { isRuntimeTokenEvidence, runtimeObservationFromTaskEvent } from "./runtimeObservation.js";
3
4
  import { projectSessionTokenObservations, unobservedSessionTokenMetrics } from "./sessionTokenMetrics.js";
@@ -39,7 +40,7 @@ export function projectTaskUsageMetrics(input) {
39
40
  continue;
40
41
  const resource = resourceKey(identity);
41
42
  const owners = resourceOwners.get(resource) ?? new Set();
42
- owners.add(identity.roleName);
43
+ owners.add(JSON.stringify([identity.roleName, identity.agentId]));
43
44
  resourceOwners.set(resource, owners);
44
45
  }
45
46
  const sessions = [...buckets.values()]
@@ -107,14 +108,21 @@ export function projectTaskUsageMetrics(input) {
107
108
  }];
108
109
  });
109
110
  const reasons = [];
111
+ const costReasons = [];
110
112
  if (scoped.some((observation) => sessionIdentity(observation) === null))
111
113
  reasons.push("session-identity-missing");
114
+ if (reasons.length > 0)
115
+ costReasons.push("session-identity-missing");
112
116
  for (const run of runs.values()) {
113
117
  if (input.workItemId !== undefined && run.workItemId !== input.workItemId)
114
118
  continue;
115
119
  if (!observations.some((observation) => exactRun(observation, runs)?.id === run.id
116
120
  && usageAuthority(observation) && isRuntimeTokenEvidence(observation)))
117
121
  reasons.push("run-usage-unobserved");
122
+ if (!observations.some((observation) => exactRun(observation, runs)?.id === run.id
123
+ && usageAuthority(observation) && !child(observation) && observation.payload.cost !== undefined)) {
124
+ costReasons.push("run-cost-unobserved");
125
+ }
118
126
  }
119
127
  const tokenValues = sessions.flatMap(({ tokens }) => tokens.value === null ? [] : [tokens.value]);
120
128
  const sum = safeSum(tokenValues);
@@ -149,6 +157,7 @@ export function projectTaskUsageMetrics(input) {
149
157
  elapsedSeconds: input.workItemId === undefined
150
158
  ? taskElapsed(input.task, events, now) : metric(null, ["work-item-lifecycle-not-task-elapsed"]),
151
159
  executionSeconds: executionDuration(scoped, now),
160
+ costs: projectTaskCosts(observations, inScope, input.workItemId !== undefined, costReasons),
152
161
  coverage: {
153
162
  basis: "task-fenced-observations",
154
163
  observedSessions: sessions.length,
@@ -199,7 +208,7 @@ function identityKey(identity) {
199
208
  return JSON.stringify([identity.taskId, identity.roleName, identity.agentId, identity.driverId, identity.nativeSessionId]);
200
209
  }
201
210
  function resourceKey(identity) {
202
- return JSON.stringify([identity.agentId, identity.driverId, identity.nativeSessionId]);
211
+ return JSON.stringify([identity.driverId, identity.nativeSessionId]);
203
212
  }
204
213
  function child({ fence }) {
205
214
  return fence.continuationId !== undefined || fence.parentContinuationId !== undefined;
@@ -0,0 +1,38 @@
1
+ import { projectTaskUsageMetrics } from "./taskUsageMetrics.js";
2
+ export const TASK_USAGE_FACT_LIMIT = 2000;
3
+ /** Same bounded, read-only fact query for the Task CLI and Web. */
4
+ export function readTaskUsage(store, taskId, options = {}) {
5
+ const task = store.getTask(taskId);
6
+ if (task === null)
7
+ return null;
8
+ const facts = store.readTaskUsageFacts(taskId);
9
+ return pageTaskUsage(projectTaskUsageMetrics({ task, ...facts, now: options.now }), facts.complete, options);
10
+ }
11
+ export function pageTaskUsage(input, complete = true, options = {}) {
12
+ const offset = options.offset ?? 0, limit = options.limit ?? 0;
13
+ if (!Number.isSafeInteger(offset) || offset < 0 || !Number.isSafeInteger(limit) || limit < 0 || limit > 50) {
14
+ throw new Error("Usage detail offset must be non-negative; limit must be 0..50.");
15
+ }
16
+ const bounded = (metric) => complete ? metric : {
17
+ ...metric, status: metric.value === null ? "unknown" : "partial",
18
+ reasons: [...new Set([...metric.reasons, "usage-history-limited"])]
19
+ };
20
+ const cost = (value) => ({
21
+ ...value,
22
+ status: complete || value.status === "unknown" ? value.status : "partial",
23
+ reasons: complete ? value.reasons : [...new Set([...value.reasons, "usage-history-limited"])],
24
+ amounts: value.amounts.map(amount => ({ ...bounded(amount), currency: amount.currency })),
25
+ evidence: value.evidence.slice(offset, offset + limit)
26
+ });
27
+ const total = Math.max(input.sessions.length, input.costs.actual.evidence.length, input.costs.estimated.evidence.length);
28
+ return {
29
+ ...input, tokens: bounded(input.tokens), toolCalls: bounded(input.toolCalls),
30
+ executionSeconds: bounded(input.executionSeconds),
31
+ costs: { scope: input.costs.scope, actual: cost(input.costs.actual), estimated: cost(input.costs.estimated) },
32
+ sessions: input.sessions.slice(offset, offset + limit),
33
+ details: { offset, limit, sessionTotal: input.sessions.length,
34
+ actualTotal: input.costs.actual.evidence.length, estimatedTotal: input.costs.estimated.evidence.length,
35
+ nextOffset: offset + limit < total ? offset + limit : null },
36
+ history: { complete, limit: TASK_USAGE_FACT_LIMIT }
37
+ };
38
+ }
@@ -36,6 +36,7 @@ export function buildTaskExecutionProjection(store, taskId, now = new Date()) {
36
36
  });
37
37
  return projectTaskExecution({
38
38
  task,
39
+ usageFacts: store.readTaskUsageFacts(taskId),
39
40
  roles,
40
41
  runs,
41
42
  usageRuns,
@@ -110,6 +111,7 @@ export function projectTaskExecution(facts) {
110
111
  workItems,
111
112
  executionGroups: observabilityGroups,
112
113
  runs: facts.usageRuns ?? runs,
114
+ usageFacts: facts.usageFacts,
113
115
  events,
114
116
  contextSnapshots: facts.contextSnapshots,
115
117
  sessionTokens,
@@ -1,4 +1,5 @@
1
1
  import { projectTaskUsageMetrics } from "../runtime/taskUsageMetrics.js";
2
+ import { pageTaskUsage } from "../runtime/taskUsageQuery.js";
2
3
  /**
3
4
  * Build the read-only DAG, execution, cost, and context view consumed by CLI
4
5
  * and Web. It deliberately derives every value from existing Task records and
@@ -145,8 +146,15 @@ function projectCost(input, groups, workItemId) {
145
146
  const groupIds = new Set(uniqueGroups.map(({ id }) => id));
146
147
  const attempts = input.runs.filter(({ executionGroupId }) => (executionGroupId !== undefined && groupIds.has(executionGroupId)));
147
148
  const laneCount = uniqueGroups.reduce((total, group) => total + group.lanes.length, 0);
149
+ const usage = projectTaskUsageMetrics({
150
+ ...input,
151
+ ...(input.usageFacts === undefined ? {} : {
152
+ events: input.usageFacts.events, runs: input.usageFacts.runs
153
+ }),
154
+ workItemId
155
+ });
148
156
  return Object.freeze({
149
- ...projectTaskUsageMetrics({ ...input, workItemId }),
157
+ ...(input.usageFacts === undefined ? usage : pageTaskUsage(usage, input.usageFacts.complete)),
150
158
  laneCount,
151
159
  groupCount: uniqueGroups.length,
152
160
  retryCount: Math.max(0, attempts.length - laneCount),
@@ -1,5 +1,6 @@
1
1
  import { isAbsolute, join, relative, resolve } from "node:path";
2
2
  import { createHash } from "node:crypto";
3
+ import { statSync } from "node:fs";
3
4
  /**
4
5
  * The single authority for every Yui self-managed path derived from a Home.
5
6
  *
@@ -64,6 +65,18 @@ export function integrationTmuxSocketRoot(home) {
64
65
  const homeDigest = createHash("sha256").update(homeRoot(home)).digest("hex").slice(0, 16);
65
66
  return join("/tmp", `yi-${uid.toString(36)}-${homeDigest}`);
66
67
  }
68
+ /** Short IPC endpoint for one foreground HTTP Job. Its durable owner and
69
+ * receipts stay in the Job artifact directory; only the socket lives here. */
70
+ export function jobPreviewSocketRoot(artifactDir) {
71
+ const uid = typeof process.getuid === "function" ? process.getuid() : 0;
72
+ const identity = statSync(artifactDir);
73
+ if (!identity.isDirectory())
74
+ throw new Error("Preview Job artifacts must be a directory.");
75
+ const owner = createHash("sha256").update(JSON.stringify([
76
+ resolve(artifactDir), identity.dev, identity.ino
77
+ ])).digest("hex").slice(0, 24);
78
+ return join("/tmp", `yp-${uid.toString(36)}-${owner}`);
79
+ }
67
80
  /**
68
81
  * Disposable staging prefix for `yui update`'s side-by-side package install
69
82
  * (an upgrade artifact): `<home>/runtime/update-staging`. Kept under Home so a
@@ -2,6 +2,7 @@ import { createHash } from "node:crypto";
2
2
  import Database from "better-sqlite3";
3
3
  import { BASELINE_SCHEMA_SQL, SQLITE_SCHEMA_TABLES } from "./baselineSchema.js";
4
4
  import { migrateTaskMainWorkspaces } from "./upgrade/taskMainWorkspaceMigration.js";
5
+ import { migrateProjectKnowledge } from "./upgrade/projectKnowledgeMigration.js";
5
6
  import { CURRENT_STORAGE_VERSION, MIN_SUPPORTED_STORAGE_VERSION, STORAGE_FORMAT, isMinorStorageUpgrade, isStorageVersion, storageVersionParts } from "./storageVersions.js";
6
7
  export { BASELINE_SCHEMA_SQL, SQLITE_SCHEMA_TABLES };
7
8
  export const TELEMETRY_KEEP_PER_RUN = 200;
@@ -34,6 +35,47 @@ const MINOR_UPGRADES = Object.freeze([{
34
35
  // New grants can carry source evidence. Valid historical Operator grants
35
36
  // remain unchanged; migration must not invent authorization provenance.
36
37
  sql: ""
38
+ }, {
39
+ fromVersion: "1.2",
40
+ toVersion: "1.3",
41
+ name: "selected-session-input",
42
+ introducedIn: "next",
43
+ sourceChecksum: CURRENT_SCHEMA_CHECKSUM,
44
+ targetChecksum: CURRENT_SCHEMA_CHECKSUM,
45
+ // Optional explicit Session fences apply only to new inputs. Preserve old
46
+ // Role-addressed messages; never infer a historical user's Session choice.
47
+ sql: ""
48
+ }, {
49
+ fromVersion: "1.3",
50
+ toVersion: "1.4",
51
+ name: "project-knowledge-applicability",
52
+ introducedIn: "next",
53
+ sourceChecksum: CURRENT_SCHEMA_CHECKSUM,
54
+ targetChecksum: CURRENT_SCHEMA_CHECKSUM,
55
+ sql: "",
56
+ dataMigration: "project-knowledge"
57
+ }, {
58
+ fromVersion: "1.4",
59
+ toVersion: "1.5",
60
+ name: "frozen-skill-packages",
61
+ introducedIn: "next",
62
+ sourceChecksum: CURRENT_SCHEMA_CHECKSUM,
63
+ targetChecksum: CURRENT_SCHEMA_CHECKSUM,
64
+ // Additive effective-launch/manifest package evidence. Historical envelopes
65
+ // and their digests remain unchanged: absence explicitly means entrypoint-only
66
+ // provenance, not an invitation to invent packages from today's source files.
67
+ // Existing Hosts retain their original inputs. Restoring such a Session fails
68
+ // with a bounded new-Session instruction; ordinary readers never backfill it.
69
+ sql: ""
70
+ }, {
71
+ fromVersion: "1.5",
72
+ toVersion: "1.6",
73
+ name: "runtime-cost-evidence",
74
+ introducedIn: "next",
75
+ sourceChecksum: CURRENT_SCHEMA_CHECKSUM,
76
+ targetChecksum: CURRENT_SCHEMA_CHECKSUM,
77
+ // Optional sourced monetary observations; historical missing costs stay unknown.
78
+ sql: ""
37
79
  }]);
38
80
  export function storageMinorUpgradePlan(from) {
39
81
  if (from === CURRENT_STORAGE_VERSION)
@@ -143,6 +185,8 @@ export function applySqliteMinorUpgrades(db, home, createdRoots = []) {
143
185
  if (step.dataMigration === "task-main-workspace") {
144
186
  migrateTaskMainWorkspaces(db, home, createdRoots);
145
187
  }
188
+ if (step.dataMigration === "project-knowledge")
189
+ migrateProjectKnowledge(db);
146
190
  const { major, minor } = storageVersionParts(step.toVersion);
147
191
  db.prepare("UPDATE storage_schema SET major=?,minor=?,checksum=? WHERE id=1")
148
192
  .run(major, minor, step.targetChecksum);
@@ -66,6 +66,7 @@ import { initializeSqliteSchema, validateSqliteSchema, SqliteSchemaError, TELEME
66
66
  import { StorageSchemaError } from "./storageSchema.js";
67
67
  import { STORAGE_FORMAT } from "./storageVersions.js";
68
68
  import { queryTaskCatalog } from "./taskCatalog.js";
69
+ import { queryTaskSearch } from "./taskSearch.js";
69
70
  import { CURRENT_CONFIG_SCHEMA_VERSION, CURRENT_WORK_MAILBOX_SCHEMA_VERSION, executionLaneActiveRunKey, executionLaneActiveRunKeyParts, isValidCapabilityGrantTransition, isValidReleaseWorkflowTransition, pendingWakeupProjection, StorageCancelledError, StorageConflictError, StorageRecordError, storedCapabilityGrant, storedPublicationReference, storedReleaseWorkflow, validateYuiConfig } from "./taskStore.js";
70
71
  /** Read the immutable Home identity without opening a writable Store connection. */
71
72
  export function readSqliteHomeIdentity(rootDir, databaseFilename = "yui.db") {
@@ -253,6 +254,55 @@ export class SqliteTaskStore {
253
254
  listEventsByType(taskId, types) {
254
255
  return this.#sortById(this.#listPayload("events", "task_id = ? AND type IN (SELECT value FROM json_each(?))", [taskId, JSON.stringify(types)]), event => event.id);
255
256
  }
257
+ readTaskUsageFacts(taskId) {
258
+ // Bounded source selection, not a scan/deserialization of Task history.
259
+ // Strip result bodies before they cross the SQLite boundary.
260
+ // Each event type uses idx_events_type_time (including its rowid tie
261
+ // breaker); only bounded prefixes are merged before JSON projection.
262
+ const limit = 2000;
263
+ const rows = this.#db.prepare(`SELECT json_set(payload, '$.payload.observation',
264
+ CASE WHEN type = 'runtime.observation' THEN
265
+ json_remove(json_extract(payload, '$.payload.observation'),
266
+ '$.payload.output', '$.payload.input', '$.payload.failure.lastOutput', '$.payload.summary')
267
+ ELSE NULL END) AS payload
268
+ FROM (
269
+ SELECT * FROM (
270
+ SELECT * FROM (
271
+ SELECT rowid AS source_order, type, occurred_at, payload FROM events
272
+ WHERE task_id = ? AND type = 'runtime.observation'
273
+ ORDER BY occurred_at DESC, rowid DESC LIMIT ?
274
+ )
275
+ UNION ALL
276
+ SELECT * FROM (
277
+ SELECT rowid AS source_order, type, occurred_at, payload FROM events
278
+ WHERE task_id = ? AND type = 'task.cancelled'
279
+ ORDER BY occurred_at DESC, rowid DESC LIMIT ?
280
+ )
281
+ ) ORDER BY occurred_at DESC, source_order DESC LIMIT ?
282
+ )`).all(taskId, limit + 1, taskId, limit + 1, limit + 1);
283
+ const events = rows.slice(0, limit).map(row => {
284
+ const event = JSON.parse(row.payload);
285
+ // json_set embeds normalized JSON as an object; the event wire contract
286
+ // stores the canonical observation in a string.
287
+ const observation = event.payload.observation;
288
+ return typeof observation === "object" && observation !== null
289
+ ? { ...event, payload: { ...event.payload, observation: JSON.stringify(observation) } } : event;
290
+ });
291
+ // The Task/Run primary key supplies a deterministic bounded ID window,
292
+ // not a "latest by time" claim. Beyond the cap coverage is partial.
293
+ const runs = this.#db.prepare(`SELECT json_object(
294
+ 'id', turn_id, 'taskId', task_id, 'roleName', json_extract(payload, '$.roleName'),
295
+ 'workItemId', json_extract(payload, '$.workItemId'),
296
+ 'effective', json_object('agentId', json_extract(payload, '$.effective.agentId'),
297
+ 'adapterId', json_extract(payload, '$.effective.adapterId'))) AS payload
298
+ FROM (
299
+ SELECT turn_id, task_id, payload FROM turns WHERE task_id = ?
300
+ ORDER BY turn_id DESC LIMIT ?
301
+ )`)
302
+ .all(taskId, limit + 1);
303
+ return { events, runs: runs.slice(0, limit).map(row => JSON.parse(row.payload)),
304
+ complete: rows.length <= limit && runs.length <= limit };
305
+ }
256
306
  /** Delivery needs original workspace bases, never complete Run reports. */
257
307
  listTaskRunWorkspaceBases(taskId) {
258
308
  return this.#db.prepare(`SELECT json_object(
@@ -978,6 +1028,9 @@ export class SqliteTaskStore {
978
1028
  queryTaskCatalog(query) {
979
1029
  return queryTaskCatalog(this.#db, query);
980
1030
  }
1031
+ queryTaskSearch(query) {
1032
+ return queryTaskSearch(this.#db, query);
1033
+ }
981
1034
  listTaskChoices() {
982
1035
  const rows = this.#db.prepare(`SELECT c.task_id AS id, c.status,
983
1036
  json_extract(r.payload, '$.title') AS title
@@ -1,4 +1,4 @@
1
- export const CURRENT_STORAGE_VERSION = "1.2";
1
+ export const CURRENT_STORAGE_VERSION = "1.6";
2
2
  export const MIN_SUPPORTED_STORAGE_VERSION = "1.0";
3
3
  export const STORAGE_FORMAT = "yui-home";
4
4
  export function isStorageVersion(value) {
@@ -53,8 +53,10 @@ const READ_ONLY_STORE_METHODS = new Set([
53
53
  "readNextActionFacts",
54
54
  "readCompletionReadinessFacts",
55
55
  "queryContextRecords",
56
+ "queryTaskSearch",
56
57
  "latestEventSequence",
57
58
  "listEventsByType",
59
+ "readTaskUsageFacts",
58
60
  "listTaskRunWorkspaceBases",
59
61
  "contextInputReferences",
60
62
  "listActiveRuns",
@@ -0,0 +1,35 @@
1
+ export const TASK_SEARCH_KINDS = ["brief", "message", "decision", "result", "completion"];
2
+ /** A read-only view of authoritative JSON, not a persisted index. Apply Task
3
+ * scope before body matching and return only page-sized snippets/identities.
4
+ * No derived state needs rebuilding when a source changes or is removed. */
5
+ export function queryTaskSearch(db, input) {
6
+ if (!Number.isSafeInteger(input.limit) || input.limit < 1 || input.limit > 101) {
7
+ throw new Error("Task search query exceeds its bounded contract.");
8
+ }
9
+ const branches = [
10
+ `SELECT task_id AS taskId, 'brief' AS kind, 'task-brief' AS store, task_id AS refId,
11
+ j.fullkey AS field, j.value AS body FROM task_records, json_tree(brief) j
12
+ WHERE j.type = 'text' AND (j.key IN ('objective','technicalApproach','currentFocus','leaderSummary')
13
+ OR j.path = '$.boundaries')`,
14
+ `SELECT task_id, 'message', 'task-message', message_id, '$.body', json_extract(payload, '$.body') FROM messages`,
15
+ `SELECT task_id, 'decision', 'task-decision', decision_id, j.fullkey, j.value
16
+ FROM decisions, json_each(payload) j WHERE j.key IN ('title','rationale','supersededReason') AND j.type = 'text'`,
17
+ `SELECT task_id, 'result', 'run', turn_id, '$.result.output', json_extract(payload, '$.result.output') FROM turns`,
18
+ `SELECT task_id, 'completion', 'task', task_id, '$.completionSummary',
19
+ json_extract(payload, '$.completionSummary') FROM task_records`
20
+ ];
21
+ const params = {
22
+ query: input.query, taskId: input.taskId ?? null, kind: input.kind ?? null, limit: input.limit,
23
+ afterTask: input.after?.taskId ?? "", afterKind: input.after?.kind ?? "",
24
+ afterRef: input.after?.refId ?? "", afterField: input.after?.field ?? ""
25
+ };
26
+ // SQLite lower() folds ASCII only; other Unicode is matched literally.
27
+ // Positions and substr use Unicode code points, not UTF-16 units or bytes.
28
+ return db.prepare(`WITH sources AS (${branches.join(" UNION ALL ")}),
29
+ matches AS (SELECT *, instr(lower(body), lower(@query)) AS hit FROM sources
30
+ WHERE (@taskId IS NULL OR taskId = @taskId) AND (@kind IS NULL OR kind = @kind)
31
+ AND (taskId, kind, refId, field) > (@afterTask, @afterKind, @afterRef, @afterField))
32
+ SELECT taskId, kind, store, refId, field, max(0, hit - 81) AS offset,
33
+ substr(body, max(1, hit - 80), 320) AS snippet
34
+ FROM matches WHERE hit > 0 ORDER BY taskId, kind, refId, field LIMIT @limit`).all(params);
35
+ }
@@ -0,0 +1,52 @@
1
+ import { createHash } from "node:crypto";
2
+ import { knowledgeProposalFingerprint, validateProject } from "../../repository/project.js";
3
+ /**
4
+ * The declared 1.3 -> 1.4 payload transition. Never used by ordinary readers.
5
+ * Version 1 is the observed pre-migration head, not a claim that no earlier
6
+ * edits happened. Existing proposals and frozen Context snapshots retain their
7
+ * original identities, decisions and evidence; snapshots are never rewritten.
8
+ */
9
+ export function migrateProjectKnowledge(db) {
10
+ const rows = db.prepare("SELECT id, payload FROM projects ORDER BY id").all();
11
+ for (const row of rows) {
12
+ const project = JSON.parse(row.payload);
13
+ if (project.id !== row.id || !Array.isArray(project.knowledge) || !Array.isArray(project.knowledgeProposals)) {
14
+ throw new Error(`Invalid 1.3 Project knowledge container: ${row.id}.`);
15
+ }
16
+ const knowledgeProposals = project.knowledgeProposals.map(proposal => {
17
+ if (proposal.schemaVersion !== 1)
18
+ throw new Error(`Invalid 1.3 Knowledge proposal: ${proposal.id}.`);
19
+ const sourceKey = [proposal.projectId, proposal.source.taskId, proposal.source.decisionId ?? "",
20
+ proposal.source.milestoneId ?? "", proposal.source.commitSha ?? ""].join("|");
21
+ const previousFingerprint = createHash("sha256")
22
+ .update(`${sourceKey}\u0000${proposal.title}\u0000${proposal.body}`).digest("hex");
23
+ if (proposal.fingerprint !== previousFingerprint) {
24
+ throw new Error(`Invalid 1.3 Knowledge fingerprint: ${proposal.id}.`);
25
+ }
26
+ return { ...proposal, schemaVersion: 2, fingerprint: knowledgeProposalFingerprint(proposal) };
27
+ });
28
+ const knowledge = project.knowledge.map(entry => {
29
+ if (entry.schemaVersion !== 1 || "version" in entry || "history" in entry
30
+ || "scope" in entry || "expiresWhen" in entry) {
31
+ throw new Error(`Invalid 1.3 Project knowledge: ${entry.id}.`);
32
+ }
33
+ const proposal = knowledgeProposals.find(candidate => candidate.id === entry.provenance?.proposalId);
34
+ const backed = proposal !== undefined && proposal.status === "accepted"
35
+ && proposal.knowledgeId === entry.id && proposal.title === entry.title && proposal.body === entry.body
36
+ && proposal.source.taskId === entry.provenance?.taskId
37
+ && proposal.source.decisionId === entry.provenance?.decisionId
38
+ && proposal.source.milestoneId === entry.provenance?.milestoneId
39
+ && proposal.source.commitSha === entry.provenance?.commitSha;
40
+ return {
41
+ ...entry, schemaVersion: 2, version: 1, history: [],
42
+ ...(backed ? {
43
+ ...(proposal.scope === undefined ? {} : { scope: proposal.scope }),
44
+ ...(proposal.expiresWhen === undefined ? {} : { expiresWhen: proposal.expiresWhen }),
45
+ provenance: { ...entry.provenance, fingerprint: proposal.fingerprint }
46
+ } : {})
47
+ };
48
+ });
49
+ const migrated = validateProject({ ...project, knowledge, knowledgeProposals });
50
+ db.prepare("UPDATE projects SET payload=? WHERE id=?").run(JSON.stringify(migrated), row.id);
51
+ }
52
+ }
@@ -0,0 +1,42 @@
1
+ import { runTaskCommand, validateTaskArchiveRequest } from "../commands/taskCommands.js";
2
+ import { taskArchiveDiagnostics } from "./archiveDiagnostics.js";
3
+ import { assertTaskRemoteDeliveryIntegrated, createTaskRemoteDeliveryProof } from "./remoteDeliveryService.js";
4
+ /** Read-only admission shared by CLI and Web. Cancelled Project Tasks need a
5
+ * workspace snapshot before delivery can be proved; that later preparation
6
+ * may have effects and must not be reported as a known non-submission. */
7
+ export function validateOrdinaryTaskArchive(store, taskId, options) {
8
+ validateTaskArchiveRequest([taskId, "--integrated"], store, options);
9
+ const task = store.getTask(taskId);
10
+ if (task.status !== "archived" && (task.status === "completed" || !task.projectBindings.length)) {
11
+ assertTaskRemoteDeliveryIntegrated(createTaskRemoteDeliveryProof(store, task).delivery);
12
+ }
13
+ }
14
+ /** The local user's ordinary archive path. No force, abandonment, source
15
+ * impersonation, or private cleanup policy is accepted by this port. */
16
+ export async function archiveOrdinaryTask(coordinator, taskId, options) {
17
+ const { store, preparer } = coordinator;
18
+ const args = [taskId, "--integrated"];
19
+ validateOrdinaryTaskArchive(store, taskId, options);
20
+ const task = store.getTask(taskId);
21
+ if (task.status !== "archived") {
22
+ let candidate = null;
23
+ if (task.status === "cancelled" && task.projectBindings.length) {
24
+ await preparer.prepareTaskWorkspace(taskId);
25
+ const workspace = store.getTaskWorkspace(taskId);
26
+ if (!workspace || workspace.owner.type !== "task" || workspace.owner.taskId !== taskId) {
27
+ throw new Error(`Task has no authoritative main workspace: ${taskId}.`);
28
+ }
29
+ const snapshot = await preparer.snapshotDirectTaskMain(workspace, task.projectBindings.map(binding => binding.projectId));
30
+ candidate = { schemaVersion: 1, projects: snapshot.projects.map(project => ({
31
+ projectId: project.projectId, commit: project.headCommit
32
+ })) };
33
+ }
34
+ const proof = createTaskRemoteDeliveryProof(store, task, candidate);
35
+ assertTaskRemoteDeliveryIntegrated(proof.delivery);
36
+ await coordinator.prepareTaskForArchive(taskId);
37
+ runTaskCommand(["archive", ...args], store, { ...options, archiveRemoteDeliveryProof: proof });
38
+ await coordinator.cleanupArchivedTask(taskId, "integrated");
39
+ }
40
+ const current = store.getTask(taskId);
41
+ return { task: current, ...taskArchiveDiagnostics(store, current) };
42
+ }
@@ -263,16 +263,16 @@ export class TmuxManager {
263
263
  // with terminal output that predates this attach.
264
264
  this.run(["set-option", "-t", clientSession, "status", "off"]);
265
265
  this.run(["set-option", "-t", clientSession, "mouse", "on"]);
266
+ // Arm automatic release only after a client has attached. Enabling it
267
+ // on the initially detached session would destroy it before attach.
268
+ // A client-detached hook is unsafe: after explicit session destruction,
269
+ // tmux can resolve that notification in a surviving session's context
270
+ // and execute its hook, cascading into unrelated viewer/writer leases.
266
271
  this.run([
267
- "set-hook", "-t", clientSession, "client-detached",
268
- `kill-session -t ${clientSession}`
272
+ "set-hook", "-t", clientSession, "client-attached",
273
+ `set-option -t ${clientSession} destroy-unattached on`
274
+ + (attachedNotice === undefined ? "" : ` ; display-message -d 10000 ${tmuxWord(attachedNotice)}`)
269
275
  ]);
270
- if (attachedNotice !== undefined) {
271
- this.run([
272
- "set-hook", "-t", clientSession, "client-attached",
273
- `display-message -d 10000 ${tmuxWord(attachedNotice)}`
274
- ]);
275
- }
276
276
  return clientSession;
277
277
  }
278
278
  catch (error) {
@@ -1,63 +1,23 @@
1
+ // Every asset the dashboard serves, keyed by URL path. Stylesheets come from
2
+ // the ordered styles registry, client modules from the client registry, and
3
+ // fonts and xterm from their packages; the shell links them in this order.
1
4
  import { readFileSync } from "node:fs";
2
5
  import { createRequire } from "node:module";
3
- import { API_SCRIPT } from "./client/api.js";
4
- import { APP_SCRIPT } from "./client/app.js";
5
- import { COMPONENTS_SCRIPT } from "./client/components.js";
6
- import { DETAIL_SCRIPT } from "./client/detail.js";
7
- import { DOCK_SCRIPT } from "./client/dock.js";
8
- import { DOM_SCRIPT } from "./client/dom.js";
9
- import { EVIDENCE_SCRIPT } from "./client/evidence.js";
10
- import { FORMAT_SCRIPT } from "./client/format.js";
11
- import { FORMS_SCRIPT } from "./client/forms.js";
12
- import { I18N_SCRIPT } from "./client/i18n.js";
13
- import { MARKDOWN_SCRIPT } from "./client/markdown.js";
14
- import { OVERVIEW_SCRIPT } from "./client/overview.js";
15
- import { RECORDS_SCRIPT } from "./client/records.js";
16
- import { SECTIONS_SCRIPT } from "./client/sections.js";
17
- import { SIDEBAR_SCRIPT } from "./client/sidebar.js";
18
- import { THEME_SCRIPT } from "./client/theme.js";
19
- import { DASHBOARD_HTML } from "./shell.js";
20
- import { FONT_FACE_STYLES } from "./fonts.js";
6
+ import { APP_SCRIPT, CLIENT_MODULES } from "./client/index.js";
21
7
  import { FONT_WOFF2_BASE64 } from "./fontData.js";
22
- import { BASE_STYLES } from "./styles/base.js";
23
- import { COMPONENT_STYLES } from "./styles/components.js";
24
- import { LAYOUT_STYLES } from "./styles/layout.js";
25
- import { MARKDOWN_STYLES } from "./styles/markdown.js";
26
- import { RESPONSIVE_STYLES } from "./styles/responsive.js";
27
- import { TOKEN_STYLES } from "./styles/tokens.js";
28
- import { VIEW_STYLES } from "./styles/views.js";
8
+ import { DASHBOARD_HTML } from "./shell/index.js";
9
+ import { STYLESHEETS } from "./styles/index.js";
29
10
  const require = createRequire(import.meta.url);
30
11
  const CSS = "text/css; charset=utf-8";
31
12
  const JS = "text/javascript; charset=utf-8";
32
13
  export const WEB_ASSETS = Object.freeze({
33
- "/assets/css/fonts.css": { contentType: CSS, body: FONT_FACE_STYLES },
34
- "/assets/css/tokens.css": { contentType: CSS, body: TOKEN_STYLES },
35
- "/assets/css/base.css": { contentType: CSS, body: BASE_STYLES },
36
- "/assets/css/layout.css": { contentType: CSS, body: LAYOUT_STYLES },
37
- "/assets/css/components.css": { contentType: CSS, body: COMPONENT_STYLES },
38
- "/assets/css/views.css": { contentType: CSS, body: VIEW_STYLES },
39
- "/assets/css/markdown.css": { contentType: CSS, body: MARKDOWN_STYLES },
40
- "/assets/css/responsive.css": { contentType: CSS, body: RESPONSIVE_STYLES },
41
- "/assets/js/dom.js": { contentType: JS, body: DOM_SCRIPT },
42
- "/assets/js/format.js": { contentType: JS, body: FORMAT_SCRIPT },
43
- "/assets/js/i18n.js": { contentType: JS, body: I18N_SCRIPT },
44
- "/assets/js/markdown.js": { contentType: JS, body: MARKDOWN_SCRIPT },
45
- "/assets/js/theme.js": { contentType: JS, body: THEME_SCRIPT },
46
- "/assets/js/api.js": { contentType: JS, body: API_SCRIPT },
47
- "/assets/js/components.js": { contentType: JS, body: COMPONENTS_SCRIPT },
48
- "/assets/js/records.js": { contentType: JS, body: RECORDS_SCRIPT },
49
- "/assets/js/forms.js": { contentType: JS, body: FORMS_SCRIPT },
50
- "/assets/js/evidence.js": { contentType: JS, body: EVIDENCE_SCRIPT },
51
- "/assets/js/sidebar.js": { contentType: JS, body: SIDEBAR_SCRIPT },
52
- "/assets/js/overview.js": { contentType: JS, body: OVERVIEW_SCRIPT },
53
- "/assets/js/sections.js": { contentType: JS, body: SECTIONS_SCRIPT },
54
- "/assets/js/detail.js": { contentType: JS, body: DETAIL_SCRIPT },
55
- "/assets/js/dock.js": { contentType: JS, body: DOCK_SCRIPT },
14
+ ...Object.fromEntries(STYLESHEETS.map(({ name, body }) => [`/assets/css/${name}.css`, { contentType: CSS, body }])),
15
+ ...Object.fromEntries(Object.entries(CLIENT_MODULES).map(([path, body]) => [`/assets/js/${path}.js`, { contentType: JS, body }])),
56
16
  "/assets/app.js": { contentType: JS, body: APP_SCRIPT },
57
17
  ...fontAssets(),
58
- "/assets/vendor/xterm.mjs": vendorAsset("@xterm/xterm/lib/xterm.mjs", "text/javascript; charset=utf-8"),
59
- "/assets/vendor/addon-fit.mjs": vendorAsset("@xterm/addon-fit/lib/addon-fit.mjs", "text/javascript; charset=utf-8"),
60
- "/assets/vendor/xterm.css": vendorAsset("@xterm/xterm/css/xterm.css", "text/css; charset=utf-8")
18
+ "/assets/vendor/xterm.mjs": vendorAsset("@xterm/xterm/lib/xterm.mjs", JS),
19
+ "/assets/vendor/addon-fit.mjs": vendorAsset("@xterm/addon-fit/lib/addon-fit.mjs", JS),
20
+ "/assets/vendor/xterm.css": vendorAsset("@xterm/xterm/css/xterm.css", CSS)
61
21
  });
62
22
  export { DASHBOARD_HTML };
63
23
  export function findWebAsset(pathname) {