@openspecui/core 4.1.0 → 6.0.0-beta.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 (116) hide show
  1. package/dist/cli-projection-DxnVGm2B.d.mts +789 -0
  2. package/dist/cli-projection-vO3-FJg5.mjs +161 -0
  3. package/dist/cli-projection.d.mts +6 -0
  4. package/dist/cli-projection.mjs +4 -0
  5. package/dist/command-result-fkH3Ekdz.d.mts +8477 -0
  6. package/dist/common-D8YUK9dA.mjs +57 -0
  7. package/dist/dashboard-summary-transport-CQ4is5_e.d.mts +66227 -0
  8. package/dist/dashboard-summary-transport-D2VBAIxg.mjs +175 -0
  9. package/dist/dashboard-summary-transport.d.mts +25 -0
  10. package/dist/dashboard-summary-transport.mjs +6 -0
  11. package/dist/{document-translation-D4YK18I3.mjs → document-translation-CeJFGMoy.mjs} +1 -1
  12. package/dist/{document-translation-CIPGsZD8.d.mts → document-translation-D50BnvVh.d.mts} +100 -100
  13. package/dist/document-translation.d.mts +1 -1
  14. package/dist/document-translation.mjs +2 -2
  15. package/dist/{hosted-app-CzMRiEgY.mjs → hosted-app-BPU4OR41.mjs} +31 -9
  16. package/dist/hosted-app.d.mts +25 -2
  17. package/dist/hosted-app.mjs +7 -2
  18. package/dist/hosted-contract-CcRnb51e.mjs +234 -0
  19. package/dist/hosted-contract.d.mts +25 -0
  20. package/dist/hosted-contract.mjs +7 -0
  21. package/dist/index.d.mts +25 -2686
  22. package/dist/index.mjs +2114 -1098
  23. package/dist/{local-download-profiles-B30HrSO2.d.mts → local-download-profiles-BL_rtlVM.d.mts} +1 -1
  24. package/dist/local-download-profiles.d.mts +2 -2
  25. package/dist/local-download-profiles.mjs +1 -1
  26. package/dist/markdown-facts.d.mts +1 -1
  27. package/dist/markdown-facts.mjs +1 -1
  28. package/dist/{markdown-reading-CgXuXp6s.d.mts → markdown-reading-LUdqfbxH.d.mts} +1 -1
  29. package/dist/{markdown-reading-DdcB7run.mjs → markdown-reading-mG4OAbDO.mjs} +1 -1
  30. package/dist/markdown-reading.d.mts +2 -2
  31. package/dist/markdown-reading.mjs +2 -2
  32. package/dist/{notifications-AKAcmeKT.mjs → notifications-C2bxAzX_.mjs} +13 -2
  33. package/dist/{notifications-HYH-lklF.d.mts → notifications-CgJlLgLO.d.mts} +34 -8
  34. package/dist/notifications.d.mts +2 -2
  35. package/dist/notifications.mjs +3 -3
  36. package/dist/{openspec-annotations-CeXd0JIZ.d.mts → openspec-annotations-C58Aoi-V.d.mts} +2 -2
  37. package/dist/{openspec-annotations-D_6Pz7k4.mjs → openspec-annotations-DCfqBMal.mjs} +2 -2
  38. package/dist/openspec-annotations.d.mts +3 -3
  39. package/dist/openspec-annotations.mjs +3 -3
  40. package/dist/openspec-compat.d.mts +18 -11
  41. package/dist/openspec-compat.mjs +22 -14
  42. package/dist/{openspec-projection-B8s-KWQ7.mjs → openspec-projection-DCgToo0Q.mjs} +2 -2
  43. package/dist/{openspec-projection-BphzrwYP.d.mts → openspec-projection-RohNZI4p.d.mts} +3 -3
  44. package/dist/openspec-projection.d.mts +5 -5
  45. package/dist/openspec-projection.mjs +4 -4
  46. package/dist/opsx-display-path.d.mts +1 -1
  47. package/dist/opsx-display-path.mjs +1 -1
  48. package/dist/{opsx-entity-CFUCqev8.d.mts → opsx-entity-D7kDQsze.d.mts} +2 -2
  49. package/dist/opsx-entity.d.mts +7 -3
  50. package/dist/opsx-entity.mjs +1 -1
  51. package/dist/{opsx-schema-detail-DXKXqMQa.d.mts → opsx-schema-detail-BxDXEwDY.d.mts} +2 -2
  52. package/dist/opsx-schema-detail.d.mts +8 -4
  53. package/dist/opsx-schema-detail.mjs +1 -1
  54. package/dist/opsx-types-BJ8AjhrS.d.mts +3642 -0
  55. package/dist/opsx-workflows-BvnJqjfL.d.mts +36 -0
  56. package/dist/opsx-workflows-Ccc6U7IR.mjs +89 -0
  57. package/dist/opsx-workflows.d.mts +2 -0
  58. package/dist/opsx-workflows.mjs +3 -0
  59. package/dist/planning-cli-projection-DIRxeq2H.mjs +421 -0
  60. package/dist/planning-cli-projection.d.mts +25 -0
  61. package/dist/planning-cli-projection.mjs +9 -0
  62. package/dist/schemas-BFroDdOC.mjs +287 -0
  63. package/dist/{schemas-CNKCVc7m.d.mts → schemas-DS-JqAJ2.d.mts} +498 -52
  64. package/dist/{sounds-J6z7o4mt.d.mts → sounds-qhRgCy0F.d.mts} +5 -5
  65. package/dist/sounds.d.mts +1 -1
  66. package/dist/sounds.mjs +1 -1
  67. package/dist/spec-catalog-ZGuiZ4CE.mjs +901 -0
  68. package/dist/spec-catalog.d.mts +25 -0
  69. package/dist/spec-catalog.mjs +6 -0
  70. package/dist/store-mutation-protocol-5tfs-RmD.d.mts +727 -0
  71. package/dist/store-mutation-protocol-CSwD-xPO.mjs +63 -0
  72. package/dist/store-mutation-protocol.d.mts +2 -0
  73. package/dist/store-mutation-protocol.mjs +4 -0
  74. package/dist/{store-types-Cu41QSww.mjs → store-types-DlX1REBg.mjs} +39 -18
  75. package/dist/{store-types-BiE-Ey--.d.mts → store-types-b554wAQJ.d.mts} +22 -12
  76. package/dist/store-types.d.mts +6 -2
  77. package/dist/store-types.mjs +2 -2
  78. package/dist/task-progress-B_IWVXjA.mjs +216 -0
  79. package/dist/task-progress.d.mts +7 -4
  80. package/dist/task-progress.mjs +3 -3
  81. package/dist/{terminal-audio-TBoU2jSK.mjs → terminal-audio-d6KItUeJ.mjs} +1 -1
  82. package/dist/{terminal-audio-BeUXlQgj.d.mts → terminal-audio-tXDm_EDe.d.mts} +2 -2
  83. package/dist/terminal-audio.d.mts +2 -2
  84. package/dist/terminal-audio.mjs +2 -2
  85. package/dist/terminal-control.d.mts +2 -2
  86. package/dist/terminal-control.mjs +1 -1
  87. package/dist/{terminal-invocation-ClVh0HeM.d.mts → terminal-invocation-BOvxThym.d.mts} +203 -84
  88. package/dist/terminal-invocation.d.mts +1 -1
  89. package/dist/terminal-invocation.mjs +1 -1
  90. package/dist/terminal-theme.d.mts +1 -1
  91. package/dist/terminal-theme.mjs +1 -1
  92. package/dist/translation-language-pair.d.mts +1 -1
  93. package/dist/translation-language-pair.mjs +1 -1
  94. package/dist/{translator-D6t_LEVN.d.mts → translator-NKv1wo3o.d.mts} +208 -208
  95. package/dist/translator.d.mts +1 -1
  96. package/dist/translator.mjs +1 -1
  97. package/package.json +55 -7
  98. package/dist/hosted-app-DHs94gIR.d.mts +0 -43
  99. package/dist/opsx-types-2rgsQHsB.d.mts +0 -402
  100. package/dist/task-progress-BHn4bCoD.d.mts +0 -24
  101. package/dist/task-progress-DhHYHnTv.mjs +0 -70
  102. /package/dist/{local-download-profiles-kpNEcdF_.mjs → local-download-profiles-odzOdf96.mjs} +0 -0
  103. /package/dist/{markdown-facts-BBCTe_qq.mjs → markdown-facts-BUqLvFRf.mjs} +0 -0
  104. /package/dist/{markdown-facts-Dk7FKaaT.d.mts → markdown-facts-CCw3QdbM.d.mts} +0 -0
  105. /package/dist/{opsx-display-path-CUCOrHcM.mjs → opsx-display-path-D13XV20v.mjs} +0 -0
  106. /package/dist/{opsx-display-path-cIMKa1aw.d.mts → opsx-display-path-RXS5JTbV.d.mts} +0 -0
  107. /package/dist/{opsx-entity-c7m5CbgU.mjs → opsx-entity-Cn_Zu1J3.mjs} +0 -0
  108. /package/dist/{opsx-schema-detail-BH82X6aU.mjs → opsx-schema-detail-Beis4Ba2.mjs} +0 -0
  109. /package/dist/{sounds-CB3ubSnj.mjs → sounds-NXwveBqq.mjs} +0 -0
  110. /package/dist/{terminal-control-VpTuh17B.mjs → terminal-control-BSAlOTs8.mjs} +0 -0
  111. /package/dist/{terminal-invocation-C2MCNBhK.mjs → terminal-invocation-0p-j2eSP.mjs} +0 -0
  112. /package/dist/{terminal-theme-cevliJw2.mjs → terminal-theme-B8iRttTR.mjs} +0 -0
  113. /package/dist/{terminal-theme-hvQjTsNh.d.mts → terminal-theme-m3MSGJ7U.d.mts} +0 -0
  114. /package/dist/{translation-language-pair-DZ_WNp_M.mjs → translation-language-pair-Bbhk-LAC.mjs} +0 -0
  115. /package/dist/{translation-language-pair-DmaOfHqD.d.mts → translation-language-pair-C18GPATW.d.mts} +0 -0
  116. /package/dist/{translator-DbUT1Ayn.mjs → translator-Cjtv4tya.mjs} +0 -0
package/dist/index.mjs CHANGED
@@ -1,142 +1,60 @@
1
- import { a as normalizeOpsxEntityPath, c as parseOpsxEntityMetadata, i as isOpsxGlobPattern, n as getOpsxEntityMetadataPath, o as opsxGlobToRegex, r as getOpsxEntityRootRelativePath, s as opsxPathMatchesPattern, t as buildOpsxEntityDetail } from "./opsx-entity-c7m5CbgU.mjs";
2
- import { t as parseOpsxSchemaDetail } from "./opsx-schema-detail-BH82X6aU.mjs";
3
- import { n as toMarkdownFactKind, t as parseMarkdownFacts } from "./markdown-facts-BBCTe_qq.mjs";
4
- import { a as getMarkdownAnnotation, c as getMarkdownHeadingEnd, d as trimMarkdownSlice, i as createMarkdownReadingDocumentFromFacts, l as getMarkdownHeadingFacts, n as buildMarkdownParentMap, o as getMarkdownAnnotationsForFact, r as createMarkdownReadingDocument, s as getMarkdownFactSpan, t as MarkdownReadingPluginRegistry, u as sortMarkdownReadingPlugins } from "./markdown-reading-DdcB7run.mjs";
5
- import { a as getOpenSpecAnnotationsForFact, i as getOpenSpecAnnotation, n as annotateOpenSpecMarkdown, o as openSpecAnnotationRules, r as builtinOpenSpecReadingPlugin, t as annotateOpenSpecFacts } from "./openspec-annotations-D_6Pz7k4.mjs";
6
- import { a as getOpenSpecReadingSections, c as projectOpenSpecMarkdown, i as getOpenSpecProjectionAnnotation, n as OPEN_SPEC_SPEC_PROJECTION_ID, o as parseOpenSpecMarkdownToSpec, r as createOpenSpecReadingPlugin, s as projectAnnotatedOpenSpecToSpec, t as OPEN_SPEC_READING_SECTIONS_PROJECTION_ID } from "./openspec-projection-B8s-KWQ7.mjs";
7
- import { i as projectTasksFromMarkdownFiles, n as isMarkdownTaskSourcePath, r as parseMarkdownTasks, t as computeTaskProgress } from "./task-progress-DhHYHnTv.mjs";
8
- import { $ as shouldShowTranslationEngineInstallGate, A as TranslationEngineIdSchema, B as TranslationLocalLlamaSettingsUpdateSchema, C as TranslationDownloadGroupPlanSchema, D as TranslationEngineDependencyStatusSchema, E as TranslationEngineDependencyStateSchema, F as TranslationEngineRuntimeStateSchema, G as createTranslationEngineLifecycleStatus, H as TranslationLocalSettingsUpdateSchema, I as TranslationEngineRuntimeStatusSchema, J as getTranslationEngineManifest, K as getManagedLocalTranslationEngineManifest, L as TranslationLocalCt2SettingsSchema, M as TranslationEngineInstallLogStreamSchema, N as TranslationEngineLifecycleEventSchema, O as TranslationEngineGlobalSettingsSchema, P as TranslationEngineLifecycleStatusSchema, Q as isTranslationEngineRuntimeReady, R as TranslationLocalCt2SettingsUpdateSchema, S as TranslationDownloadFilePlanSchema, T as TranslationEngineAssetStatusSchema, U as TranslationOpenAISettingsSchema, V as TranslationLocalSettingsSchema, W as TranslationOpenAISettingsUpdateSchema, X as isManagedLocalTranslationEngineId, Y as isDirectionalManagedLocalTranslationEngineId, Z as isTranslationEngineDependencyReady, _ as SERVICE_TRANSLATION_ENGINE_IDS, a as LocalModelAssetLogSchema, b as TRANSLATION_ENGINE_MANIFESTS, c as LocalModelDownloadStatusSchema, d as LocalModelProfileLoadStateSchema, et as isBatchTranslationAbort, f as LocalModelProfileManifestFileSchema, g as ManagedLocalCatalogSourceSchema, h as LocalModelProfileStatusSchema, i as DEFAULT_TRANSLATION_ENGINE_ID, j as TranslationEngineInstallLogEventSchema, k as TranslationEngineGlobalSettingsUpdateSchema, l as LocalModelLifecycleFileStateSchema, m as LocalModelProfileManifestSchema, n as BatchTranslateInputSchema, nt as runControlledTranslationTask, o as LocalModelAssetPlanSnapshotSchema, p as LocalModelProfileManifestGroupSchema, q as getTranslationEngineLifecycleMessage, s as LocalModelAssetStateSchema, t as BatchTranslateEventSchema, tt as normalizeBatchTranslationError, u as LocalModelLifecycleGroupStateSchema, v as ServiceTranslationEngineIdSchema, w as TranslationEngineAssetStateSchema, x as TRANSLATOR_CONTRACT_VERSION, y as TRANSLATION_ENGINE_IDS, z as TranslationLocalLlamaSettingsSchema } from "./translator-DbUT1Ayn.mjs";
9
- import { a as DocumentTranslationDisplayModeSchema, c as TRANSLATION_CACHE_POLICY_VERSION, d as TranslationCacheSettingsSchema, f as TranslationCacheSettingsUpdateSchema, g as TranslationEngineProjectSettingsUpdateSchema, h as TranslationEngineProjectSettingsSchema, i as DocumentTranslationConfigUpdateSchema, l as TranslationCacheEntrySchema, m as TranslationCacheWriteInputSchema, n as DOCUMENT_TRANSLATION_DISPLAY_MODES, o as MAX_TRANSLATION_CACHE_ENTRY_LIMIT, p as TranslationCacheStatsSchema, r as DocumentTranslationConfigSchema, s as MIN_TRANSLATION_CACHE_ENTRY_LIMIT, t as DEFAULT_TRANSLATION_CACHE_ENTRY_LIMIT, u as TranslationCacheReadInputSchema } from "./document-translation-D4YK18I3.mjs";
10
- import { _ as customHashFromSoundId, a as CustomSoundHashSchema, b as soundIdFromCustomHash, c as CustomSoundMetadataSchema, f as LEGACY_SOUND_ID_MAP, g as SoundVolumeSchema, h as SoundIdSchema, i as CUSTOM_SOUND_ADD_VALUE, l as DEFAULT_BELL_SOUND_ID, m as SoundConfigIdSchema, n as BUILTIN_SOUND_OPTIONS, o as CustomSoundIdSchema, p as SILENT_SOUND_ID, r as BuiltinSoundIdSchema, s as CustomSoundMetadataFileSchema, t as BUILTIN_SOUND_IDS, u as DEFAULT_NOTIFICATION_SOUND_ID, v as getBuiltinSoundUrl, y as normalizeLegacySoundId } from "./sounds-CB3ubSnj.mjs";
11
- import { n as terminalNotificationEventToPublishInput, t as TerminalControlParser } from "./terminal-control-VpTuh17B.mjs";
12
- import { a as NotificationPublishInputSchema, c as NotificationSoundSchema, h as groupNotifications, i as NotificationGroupKeySchema, l as NotificationSourceSchema, m as getNotificationGroupLabel, n as NOTIFICATION_SOUND_VALUES, o as NotificationRecordSchema, p as getNotificationGroupKey, r as NotificationActionSchema, s as NotificationSettingsSchema, t as NOTIFICATION_SOUND_OPTIONS, u as TerminalNotificationParser } from "./notifications-AKAcmeKT.mjs";
13
- import { n as TERMINAL_BELL_SOUND_VALUES, r as TerminalBellSoundSchema, t as TERMINAL_BELL_SOUND_OPTIONS } from "./terminal-audio-TBoU2jSK.mjs";
14
- import { a as TERMINAL_THEME_VALUES, i as TERMINAL_THEME_MODE_VALUES, n as DEFAULT_TERMINAL_LIGHT_THEME, r as DEFAULT_TERMINAL_THEME_MODE, t as DEFAULT_TERMINAL_DARK_THEME } from "./terminal-theme-cevliJw2.mjs";
15
- import { i as selectLocalDownloadGroup, n as buildLocalDownloadPlanFromRepositoryFiles, r as buildLocalDownloadPlanFromRuntimeProfileFiles, t as LOCAL_MODEL_PROFILE_DEFINITIONS } from "./local-download-profiles-kpNEcdF_.mjs";
16
- import { n as inferLocalDirectionalModelLanguagePair, t as checkLocalDirectionalModelLanguagePair } from "./translation-language-pair-DZ_WNp_M.mjs";
17
- import { a as buildEmbeddedUiLaunchUrl, c as isHostedBackendHealthResponse, d as normalizeHostedAppBaseUrl, f as resolveHostedAppBaseUrl, i as buildBackendHealthPayload, l as isSupportedEmbeddedUiUrl, n as OFFICIAL_APP_BASE_URL, o as buildHostedLaunchUrl, r as OPENSPECUI_RUNTIME_CAPABILITIES, s as isBackendHealthRuntimeMetadata, t as HOSTED_SHELL_PROTOCOL_VERSION, u as normalizeEmbeddedUiUrl } from "./hosted-app-CzMRiEgY.mjs";
18
- import { n as toOpsxDisplayPath, t as VIRTUAL_PROJECT_DIRNAME } from "./opsx-display-path-CUCOrHcM.mjs";
19
- import { i as toStoreFeatureResult, n as StoreListResultSchema, r as classifyStoreCliOutput, t as StoreDoctorResultSchema } from "./store-types-Cu41QSww.mjs";
20
- import { a as TerminalInvocationSettingsSchema, c as TerminalSpawnCommandSchema, f as quoteTerminalShellArg, g as resolveTerminalShellDefaults, h as renderTerminalSpawnCommandLine, i as TerminalCommandFieldSchema, n as TERMINAL_COMMAND_FIELD_TYPE_VALUES, o as TerminalShellProfileSchema, p as renderTerminalCommandArgs, r as TERMINAL_SHELL_QUOTE_STYLE_VALUES, s as TerminalShellQuoteStyleSchema, t as BUILTIN_TERMINAL_SPAWN_COMMANDS, u as getTerminalCommandDefaultValues } from "./terminal-invocation-C2MCNBhK.mjs";
21
- import { mkdir, readFile, rename, writeFile } from "fs/promises";
1
+ import { _ as isTextLikeMime, a as DeltaSpecSchema, c as ScenarioStepSchema, d as TrackedTaskProgressSchema, f as FILE_PREVIEW_KINDS, g as isTextLikeFile, h as isPreviewableFile, i as DeltaSchema, l as SpecSchema, m as inferFilePreviewKind, n as ChangeSchema, o as RequirementSchema, p as inferFileMime, r as DeltaOperationType, s as ScenarioStepKeywordSchema, t as ChangeFileSchema, u as TaskSchema } from "./schemas-BFroDdOC.mjs";
2
+ import { a as normalizeOpsxEntityPath, c as parseOpsxEntityMetadata, i as isOpsxGlobPattern, n as getOpsxEntityMetadataPath, o as opsxGlobToRegex, r as getOpsxEntityRootRelativePath, s as opsxPathMatchesPattern, t as buildOpsxEntityDetail } from "./opsx-entity-Cn_Zu1J3.mjs";
3
+ import { t as parseOpsxSchemaDetail } from "./opsx-schema-detail-Beis4Ba2.mjs";
4
+ import { n as toMarkdownFactKind, t as parseMarkdownFacts } from "./markdown-facts-BUqLvFRf.mjs";
5
+ import { a as getMarkdownAnnotation, c as getMarkdownHeadingEnd, d as trimMarkdownSlice, i as createMarkdownReadingDocumentFromFacts, l as getMarkdownHeadingFacts, n as buildMarkdownParentMap, o as getMarkdownAnnotationsForFact, r as createMarkdownReadingDocument, s as getMarkdownFactSpan, t as MarkdownReadingPluginRegistry, u as sortMarkdownReadingPlugins } from "./markdown-reading-mG4OAbDO.mjs";
6
+ import { a as getOpenSpecAnnotationsForFact, i as getOpenSpecAnnotation, n as annotateOpenSpecMarkdown, o as openSpecAnnotationRules, r as builtinOpenSpecReadingPlugin, t as annotateOpenSpecFacts } from "./openspec-annotations-DCfqBMal.mjs";
7
+ import { a as getOpenSpecReadingSections, c as projectOpenSpecMarkdown, i as getOpenSpecProjectionAnnotation, n as OPEN_SPEC_SPEC_PROJECTION_ID, o as parseOpenSpecMarkdownToSpec, r as createOpenSpecReadingPlugin, s as projectAnnotatedOpenSpecToSpec, t as OPEN_SPEC_READING_SECTIONS_PROJECTION_ID } from "./openspec-projection-DCgToo0Q.mjs";
8
+ import { a as isMarkdownTaskSourcePath, c as toggleMarkdownTask, i as deriveTrackedTaskPhase, n as createDocumentChecklistSummary, o as parseMarkdownTasks, r as createTrackedTaskProgress, s as projectTaskProjectionsFromMarkdownFiles, t as createApplyInstructionProgress } from "./task-progress-B_IWVXjA.mjs";
9
+ import { a as CliRootSchema, i as CliReferenceIndexEntrySchema, n as CliDiagnosticSchema, o as CliRootSourceSchema, r as CliDoctorReferenceEntrySchema, t as CliDiagnosticFailureSchema } from "./common-D8YUK9dA.mjs";
10
+ import { A as CliSchemasSchema, B as CliStoreCleanupSchema, C as CliArtifactInstructionsSuccessSchema, F as CliValidateSchema, G as CliStoreSchema, H as CliStoreDoctorSchema, I as CliWorkflowStatusSchema, K as parseCliCommandResult, L as CliWorkflowStatusSuccessSchema, M as CliShowSpecSchema, N as CliSpecListSchema, R as CliContextSchema, S as CliArtifactInstructionsSchema, T as CliChangeListSchema, U as CliStoreListSchema, V as CliStoreDoctorEntrySchema, W as CliStoreMutationSchema, _ as OpenSpecCliContractExecutor, a as SpecDocumentProjectionSchema, b as CliApplyInstructionsSuccessSchema, c as createStaticSpecCatalogOwnedProjection, d as getSpecCatalogEntry, f as isReferencedSpecIdentity, g as specRoutePath, h as specIdentityKey, i as SpecCommandEvidenceSchema, j as CliShowSpecDocumentSchema, l as createStaticSpecCatalogReferenceProjection, m as specIdentityFromRoute, n as ReferencedSpecIdentitySchema, o as SpecIdentitySchema, p as mergeSpecCatalog, r as SpecCatalogSchema, s as buildSpecCatalog, t as OwnedSpecIdentitySchema, u as createStaticSpecCatalogReferenceSource, x as CliArchiveSchema, y as CliApplyInstructionsSchema, z as CliDoctorSchema } from "./spec-catalog-ZGuiZ4CE.mjs";
11
+ import { a as CliProjectionInvalidationCauseSchema, d as toCliProjectionCommandEvidence, f as toCliProjectionFailure, i as CliProjectionFailureSchema, l as cliProjectionStates, n as CliProjectionCommandError, o as CliProjectionNoticeSchema, p as toCliProjectionNotice, r as CliProjectionCommandEvidenceSchema, s as CliProjectionStateNameSchema, t as CliJsonValueSchema, u as createCliProjectionStateSchema } from "./cli-projection-vO3-FJg5.mjs";
12
+ import { $ as shouldShowTranslationEngineInstallGate, A as TranslationEngineIdSchema, B as TranslationLocalLlamaSettingsUpdateSchema, C as TranslationDownloadGroupPlanSchema, D as TranslationEngineDependencyStatusSchema, E as TranslationEngineDependencyStateSchema, F as TranslationEngineRuntimeStateSchema, G as createTranslationEngineLifecycleStatus, H as TranslationLocalSettingsUpdateSchema, I as TranslationEngineRuntimeStatusSchema, J as getTranslationEngineManifest, K as getManagedLocalTranslationEngineManifest, L as TranslationLocalCt2SettingsSchema, M as TranslationEngineInstallLogStreamSchema, N as TranslationEngineLifecycleEventSchema, O as TranslationEngineGlobalSettingsSchema, P as TranslationEngineLifecycleStatusSchema, Q as isTranslationEngineRuntimeReady, R as TranslationLocalCt2SettingsUpdateSchema, S as TranslationDownloadFilePlanSchema, T as TranslationEngineAssetStatusSchema, U as TranslationOpenAISettingsSchema, V as TranslationLocalSettingsSchema, W as TranslationOpenAISettingsUpdateSchema, X as isManagedLocalTranslationEngineId, Y as isDirectionalManagedLocalTranslationEngineId, Z as isTranslationEngineDependencyReady, _ as SERVICE_TRANSLATION_ENGINE_IDS, a as LocalModelAssetLogSchema, b as TRANSLATION_ENGINE_MANIFESTS, c as LocalModelDownloadStatusSchema, d as LocalModelProfileLoadStateSchema, et as isBatchTranslationAbort, f as LocalModelProfileManifestFileSchema, g as ManagedLocalCatalogSourceSchema, h as LocalModelProfileStatusSchema, i as DEFAULT_TRANSLATION_ENGINE_ID, j as TranslationEngineInstallLogEventSchema, k as TranslationEngineGlobalSettingsUpdateSchema, l as LocalModelLifecycleFileStateSchema, m as LocalModelProfileManifestSchema, n as BatchTranslateInputSchema, nt as runControlledTranslationTask, o as LocalModelAssetPlanSnapshotSchema, p as LocalModelProfileManifestGroupSchema, q as getTranslationEngineLifecycleMessage, s as LocalModelAssetStateSchema, t as BatchTranslateEventSchema, tt as normalizeBatchTranslationError, u as LocalModelLifecycleGroupStateSchema, v as ServiceTranslationEngineIdSchema, w as TranslationEngineAssetStateSchema, x as TRANSLATOR_CONTRACT_VERSION, y as TRANSLATION_ENGINE_IDS, z as TranslationLocalLlamaSettingsSchema } from "./translator-Cjtv4tya.mjs";
13
+ import { a as DocumentTranslationDisplayModeSchema, c as TRANSLATION_CACHE_POLICY_VERSION, d as TranslationCacheSettingsSchema, f as TranslationCacheSettingsUpdateSchema, g as TranslationEngineProjectSettingsUpdateSchema, h as TranslationEngineProjectSettingsSchema, i as DocumentTranslationConfigUpdateSchema, l as TranslationCacheEntrySchema, m as TranslationCacheWriteInputSchema, n as DOCUMENT_TRANSLATION_DISPLAY_MODES, o as MAX_TRANSLATION_CACHE_ENTRY_LIMIT, p as TranslationCacheStatsSchema, r as DocumentTranslationConfigSchema, s as MIN_TRANSLATION_CACHE_ENTRY_LIMIT, t as DEFAULT_TRANSLATION_CACHE_ENTRY_LIMIT, u as TranslationCacheReadInputSchema } from "./document-translation-CeJFGMoy.mjs";
14
+ import { _ as customHashFromSoundId, a as CustomSoundHashSchema, b as soundIdFromCustomHash, c as CustomSoundMetadataSchema, f as LEGACY_SOUND_ID_MAP, g as SoundVolumeSchema, h as SoundIdSchema, i as CUSTOM_SOUND_ADD_VALUE, l as DEFAULT_BELL_SOUND_ID, m as SoundConfigIdSchema, n as BUILTIN_SOUND_OPTIONS, o as CustomSoundIdSchema, p as SILENT_SOUND_ID, r as BuiltinSoundIdSchema, s as CustomSoundMetadataFileSchema, t as BUILTIN_SOUND_IDS, u as DEFAULT_NOTIFICATION_SOUND_ID, v as getBuiltinSoundUrl, y as normalizeLegacySoundId } from "./sounds-NXwveBqq.mjs";
15
+ import { n as terminalNotificationEventToPublishInput, t as TerminalControlParser } from "./terminal-control-BSAlOTs8.mjs";
16
+ import { a as NotificationPublishInputSchema, c as NotificationSoundSchema, h as groupNotifications, i as NotificationGroupKeySchema, l as NotificationSourceSchema, m as getNotificationGroupLabel, n as NOTIFICATION_SOUND_VALUES, o as NotificationRecordSchema, p as getNotificationGroupKey, r as NotificationActionSchema, s as NotificationSettingsSchema, t as NOTIFICATION_SOUND_OPTIONS, u as TerminalNotificationParser } from "./notifications-C2bxAzX_.mjs";
17
+ import { n as TERMINAL_BELL_SOUND_VALUES, r as TerminalBellSoundSchema, t as TERMINAL_BELL_SOUND_OPTIONS } from "./terminal-audio-d6KItUeJ.mjs";
18
+ import { a as TERMINAL_THEME_VALUES, i as TERMINAL_THEME_MODE_VALUES, n as DEFAULT_TERMINAL_LIGHT_THEME, r as DEFAULT_TERMINAL_THEME_MODE, t as DEFAULT_TERMINAL_DARK_THEME } from "./terminal-theme-B8iRttTR.mjs";
19
+ import { i as selectLocalDownloadGroup, n as buildLocalDownloadPlanFromRepositoryFiles, r as buildLocalDownloadPlanFromRuntimeProfileFiles, t as LOCAL_MODEL_PROFILE_DEFINITIONS } from "./local-download-profiles-odzOdf96.mjs";
20
+ import { n as inferLocalDirectionalModelLanguagePair, t as checkLocalDirectionalModelLanguagePair } from "./translation-language-pair-Bbhk-LAC.mjs";
21
+ import { C as TemplateContentMapSchema, E as isGlobPattern, S as SchemaResolutionSchema, T as TemplatesSchema, _ as OpsxConfigBundleSchema, a as PlanningCliProjectionDataSchema, b as SchemaDetailSchema, c as ApplyInstructionsContextFilesSchema, d as ApplyTaskSchema, f as ArtifactInstructionsSchema, g as OpsxCliEvidenceSchema, h as DependencyInfoSchema, i as EnvironmentGlobalProjectionStateSchema, l as ApplyInstructionsProjectionSchema, m as ChangeStatusSchema, n as EnvironmentGlobalFileProjectionStateSchema, o as PlanningCliProjectionSelectorSchema, p as ArtifactStatusSchema, r as EnvironmentGlobalProjectionDataSchema, s as PlanningCliProjectionStateSchema, t as EnvironmentGlobalFileProjectionDataSchema, u as ApplyInstructionsSchema, v as OpsxStatusEvidenceSchema, w as TemplateContentSchema, x as SchemaInfoSchema, y as SchemaArtifactSchema } from "./planning-cli-projection-DIRxeq2H.mjs";
22
+ import { a as OPSX_CORE_PROFILE_WORKFLOWS, c as OPSX_WORKFLOW_TO_SKILL_DIR, i as OPSX_COMMAND_CAPABLE_WORKFLOWS, l as isOpsxCoreWorkflowSelection, n as OPSX_ARTIFACT_INPUT_ACTIONS, o as OPSX_TEXT_INPUT_ACTIONS, r as OPSX_CHANGE_INPUT_ACTIONS, s as OPSX_WORKFLOW_LABELS, t as OPSX_ALL_WORKFLOWS } from "./opsx-workflows-Ccc6U7IR.mjs";
23
+ import { a as DASHBOARD_METRIC_KEYS, c as DashboardSummaryProjectionSchema, i as DashboardSummaryProjectionStateSchema, l as DashboardTrendsProjectionSchema, n as DashboardSummaryInvalidationCauseSchema, o as DashboardGitSnapshotSchema, r as DashboardSummaryInvalidationSchema, s as DashboardOverviewSchema, t as DashboardSummaryIdentitySchema } from "./dashboard-summary-transport-D2VBAIxg.mjs";
24
+ import { i as StoreMutationStartResponseSchema, n as StoreMutationResultSchema, r as StoreMutationSchema, t as StoreMutationLifecycleEventSchema } from "./store-mutation-protocol-CSwD-xPO.mjs";
25
+ import { a as StoreListResultSchema, n as StoreDoctorResultSchema, o as classifyStoreCliResult, s as toStoreFeatureResult } from "./store-types-DlX1REBg.mjs";
26
+ import { S as isTerminalMutationStatus, b as asEnvUri, t as HOSTED_SHELL_PROTOCOL_VERSION, v as OPENSPECUI_RUNTIME_CAPABILITIES, x as hasCapability } from "./hosted-contract-CcRnb51e.mjs";
27
+ import { a as buildHostedLaunchUrl, c as isSupportedEmbeddedUiUrl, d as resolveHostedAppBaseUrl, i as buildEmbeddedUiLaunchUrl, l as normalizeEmbeddedUiUrl, n as OFFICIAL_APP_BASE_URL, o as isBackendHealthRuntimeMetadata, r as buildBackendHealthPayload, s as isHostedBackendHealthResponse, u as normalizeHostedAppBaseUrl } from "./hosted-app-BPU4OR41.mjs";
28
+ import { n as toOpsxDisplayPath, t as VIRTUAL_PROJECT_DIRNAME } from "./opsx-display-path-D13XV20v.mjs";
29
+ import { a as TerminalInvocationSettingsSchema, c as TerminalSpawnCommandSchema, f as quoteTerminalShellArg, g as resolveTerminalShellDefaults, h as renderTerminalSpawnCommandLine, i as TerminalCommandFieldSchema, n as TERMINAL_COMMAND_FIELD_TYPE_VALUES, o as TerminalShellProfileSchema, p as renderTerminalCommandArgs, r as TERMINAL_SHELL_QUOTE_STYLE_VALUES, s as TerminalShellQuoteStyleSchema, t as BUILTIN_TERMINAL_SPAWN_COMMANDS, u as getTerminalCommandDefaultValues } from "./terminal-invocation-0p-j2eSP.mjs";
30
+ import { mkdir, writeFile } from "fs/promises";
22
31
  import { dirname, join } from "path";
23
- import { AsyncLocalStorage } from "node:async_hooks";
24
- import { mkdir as mkdir$1, readFile as readFile$1, readdir, stat, writeFile as writeFile$1 } from "node:fs/promises";
25
- import { basename, dirname as dirname$1, join as join$1, matchesGlob, relative, resolve, sep } from "node:path";
32
+ import { isMap, parseDocument } from "yaml";
33
+ import { lstat, mkdir as mkdir$1, readFile, readdir, realpath, rm, stat, writeFile as writeFile$1 } from "node:fs/promises";
34
+ import { basename, dirname as dirname$1, isAbsolute, join as join$1, matchesGlob, posix, relative, resolve, sep, win32 } from "node:path";
26
35
  import { existsSync, lstatSync, readFileSync, realpathSync } from "node:fs";
36
+ import { AsyncLocalStorage } from "node:async_hooks";
27
37
  import { z } from "zod";
28
38
  import { EventEmitter } from "events";
29
39
  import { watch } from "fs";
30
40
  import { exec, execFile, spawn } from "child_process";
31
41
  import { promisify } from "util";
32
- import { homedir } from "node:os";
42
+ import { homedir, platform } from "node:os";
43
+ import { createHash, randomBytes } from "node:crypto";
33
44
 
34
- //#region src/file-preview.ts
35
- const MIME_BY_EXTENSION = new Map([
36
- [".md", "text/markdown"],
37
- [".markdown", "text/markdown"],
38
- [".mdown", "text/markdown"],
39
- [".mkd", "text/markdown"],
40
- [".txt", "text/plain"],
41
- [".log", "text/plain"],
42
- [".yaml", "application/yaml"],
43
- [".yml", "application/yaml"],
44
- [".json", "application/json"],
45
- [".jsonc", "application/json"],
46
- [".toml", "application/toml"],
47
- [".ini", "text/plain"],
48
- [".cfg", "text/plain"],
49
- [".conf", "text/plain"],
50
- [".html", "text/html"],
51
- [".htm", "text/html"],
52
- [".svg", "image/svg+xml"],
53
- [".png", "image/png"],
54
- [".jpg", "image/jpeg"],
55
- [".jpeg", "image/jpeg"],
56
- [".gif", "image/gif"],
57
- [".webp", "image/webp"],
58
- [".avif", "image/avif"],
59
- [".bmp", "image/bmp"],
60
- [".mp3", "audio/mpeg"],
61
- [".wav", "audio/wav"],
62
- [".ogg", "audio/ogg"],
63
- [".oga", "audio/ogg"],
64
- [".m4a", "audio/mp4"],
65
- [".aac", "audio/aac"],
66
- [".flac", "audio/flac"],
67
- [".mp4", "video/mp4"],
68
- [".m4v", "video/mp4"],
69
- [".webm", "video/webm"],
70
- [".mov", "video/quicktime"],
71
- [".mkv", "video/x-matroska"],
72
- [".ogv", "video/ogg"],
73
- [".pdf", "application/pdf"],
74
- [".js", "text/javascript"],
75
- [".jsx", "text/javascript"],
76
- [".ts", "text/typescript"],
77
- [".tsx", "text/typescript"],
78
- [".css", "text/css"],
79
- [".sh", "text/plain"],
80
- [".py", "text/plain"],
81
- [".rs", "text/plain"],
82
- [".go", "text/plain"],
83
- [".java", "text/plain"],
84
- [".c", "text/plain"],
85
- [".cc", "text/plain"],
86
- [".cpp", "text/plain"],
87
- [".h", "text/plain"],
88
- [".hpp", "text/plain"]
89
- ]);
90
- const TEXT_MIME_PREFIXES = ["text/"];
91
- const TEXT_MIME_VALUES = new Set([
92
- "application/json",
93
- "application/yaml",
94
- "application/toml",
95
- "application/xml",
96
- "image/svg+xml"
97
- ]);
98
- const FILE_PREVIEW_KINDS = [
99
- "markdown",
100
- "html",
101
- "image",
102
- "audio",
103
- "video",
104
- "pdf",
105
- "text",
106
- "none"
107
- ];
108
- function getExtension(path) {
109
- const normalized = path.replace(/\\/g, "/");
110
- const name = normalized.split("/").pop() ?? normalized;
111
- const dotIndex = name.lastIndexOf(".");
112
- if (dotIndex < 0) return "";
113
- return name.slice(dotIndex).toLowerCase();
114
- }
115
- function inferFileMime(path) {
116
- return MIME_BY_EXTENSION.get(getExtension(path)) ?? null;
117
- }
118
- function inferFilePreviewKind(path, mime) {
119
- const resolvedMime = mime ?? inferFileMime(path);
120
- if (!resolvedMime) return "none";
121
- if (resolvedMime === "text/markdown") return "markdown";
122
- if (resolvedMime === "text/html") return "html";
123
- if (resolvedMime.startsWith("image/")) return resolvedMime === "image/svg+xml" ? "text" : "image";
124
- if (resolvedMime.startsWith("audio/")) return "audio";
125
- if (resolvedMime.startsWith("video/")) return "video";
126
- if (resolvedMime === "application/pdf") return "pdf";
127
- if (isTextLikeMime(resolvedMime)) return "text";
128
- return "none";
129
- }
130
- function isTextLikeMime(mime) {
131
- if (!mime) return false;
132
- if (TEXT_MIME_VALUES.has(mime)) return true;
133
- return TEXT_MIME_PREFIXES.some((prefix) => mime.startsWith(prefix));
134
- }
135
- function isTextLikeFile(path, mime) {
136
- return isTextLikeMime(mime ?? inferFileMime(path));
137
- }
138
- function isPreviewableFile(path, mime) {
139
- return inferFilePreviewKind(path, mime) !== "none";
45
+ //#region src/entity-id.ts
46
+ /** Return one path-safe OpenSpec entity id or reject it before filesystem access. */
47
+ function requireCanonicalOpenSpecEntityId(value, field) {
48
+ if (value.length === 0 || value !== value.trim() || value === "." || value === ".." || value.includes("/") || value.includes("\\") || value.includes("\0")) throw new Error(`Invalid ${field}: expected one canonical path segment.`);
49
+ return value;
50
+ }
51
+ /** Return one normalized entity-relative path or glob without parent traversal. */
52
+ function requireOpenSpecEntityRelativePath(value, field) {
53
+ const normalizedSeparators = value.replace(/\\/g, "/");
54
+ if (normalizedSeparators.length === 0 || normalizedSeparators.includes("\0") || normalizedSeparators.startsWith("/") || /^[A-Za-z]:/.test(normalizedSeparators) || normalizedSeparators.split("/").includes("..")) throw new Error(`Invalid ${field}: expected an entity-relative path.`);
55
+ const normalizedPath = normalizedSeparators.split("/").filter((segment) => segment.length > 0 && segment !== ".").join("/");
56
+ if (!normalizedPath) throw new Error(`Invalid ${field}: expected an entity-relative path.`);
57
+ return normalizedPath;
140
58
  }
141
59
 
142
60
  //#endregion
@@ -206,7 +124,11 @@ var MarkdownParser = class {
206
124
  });
207
125
  }
208
126
  }
209
- const tasks = this.parseTasks(tasksContent);
127
+ const taskProjections = projectTaskProjectionsFromMarkdownFiles(tasksContent.length > 0 ? [{
128
+ path: "tasks.md",
129
+ type: "file",
130
+ content: tasksContent
131
+ }] : []);
210
132
  const deltasFromDeltaSpecs = this.parseDeltasFromDeltaSpecs(options?.deltaSpecs);
211
133
  const deltasFromWhatChanges = this.parseDeltasFromWhatChanges(whatChanges);
212
134
  const combinedDeltas = deltasFromDeltaSpecs.length > 0 ? deltasFromDeltaSpecs : deltas;
@@ -217,11 +139,8 @@ var MarkdownParser = class {
217
139
  why: why.trim(),
218
140
  whatChanges: whatChanges.trim(),
219
141
  deltas: finalDeltas,
220
- tasks,
221
- progress: {
222
- total: tasks.length,
223
- completed: tasks.filter((t) => t.completed).length
224
- },
142
+ trackedTaskProgress: taskProjections.trackedTaskProgress,
143
+ documentChecklistSummary: taskProjections.documentChecklistSummary,
225
144
  design: options?.design,
226
145
  deltaSpecs: options?.deltaSpecs
227
146
  };
@@ -381,171 +300,6 @@ var MarkdownParser = class {
381
300
  }
382
301
  };
383
302
 
384
- //#endregion
385
- //#region src/reactive-fs/reactive-state.ts
386
- /**
387
- * 全局的 AsyncLocalStorage,用于在异步调用链中传递 ReactiveContext
388
- * 这是实现依赖收集的核心机制
389
- */
390
- const contextStorage = new AsyncLocalStorage();
391
- /**
392
- * 响应式状态类,类似 Signal.State
393
- *
394
- * 核心机制:
395
- * - get() 时自动注册到当前 ReactiveContext 的依赖列表
396
- * - set() 时如果值变化,通知所有依赖的 Context
397
- */
398
- var ReactiveState = class {
399
- currentValue;
400
- equals;
401
- /** 所有依赖此状态的 Context */
402
- subscribers = /* @__PURE__ */ new Set();
403
- constructor(initialValue, options) {
404
- this.currentValue = initialValue;
405
- this.equals = options?.equals ?? ((a, b) => a === b);
406
- }
407
- /**
408
- * 获取当前值
409
- * 如果在 ReactiveContext 中调用,会自动注册依赖
410
- */
411
- get() {
412
- const context = contextStorage.getStore();
413
- if (context) {
414
- context.track(this);
415
- this.subscribers.add(context);
416
- }
417
- return this.currentValue;
418
- }
419
- /**
420
- * 设置新值
421
- * 如果值变化,通知所有订阅者
422
- * @returns 是否发生了变化
423
- */
424
- set(newValue) {
425
- if (this.equals(this.currentValue, newValue)) return false;
426
- this.currentValue = newValue;
427
- for (const context of this.subscribers) context.notifyChange();
428
- return true;
429
- }
430
- /**
431
- * 取消订阅
432
- * 当 Context 销毁时调用
433
- */
434
- unsubscribe(context) {
435
- this.subscribers.delete(context);
436
- }
437
- /**
438
- * 获取当前订阅者数量(用于调试)
439
- */
440
- get subscriberCount() {
441
- return this.subscribers.size;
442
- }
443
- };
444
-
445
- //#endregion
446
- //#region src/reactive-fs/reactive-context.ts
447
- function createPromiseWithResolvers() {
448
- let resolve$1;
449
- let reject;
450
- return {
451
- promise: new Promise((res, rej) => {
452
- resolve$1 = res;
453
- reject = rej;
454
- }),
455
- resolve: resolve$1,
456
- reject
457
- };
458
- }
459
- /**
460
- * 响应式上下文,管理依赖收集和变更通知
461
- *
462
- * 核心机制:
463
- * - 在 stream() 中执行任务时,通过 AsyncLocalStorage 传递 this
464
- * - 任务中的所有 ReactiveState.get() 调用都会自动注册依赖
465
- * - 当任何依赖变更时,重新执行任务并 yield 新结果
466
- */
467
- var ReactiveContext = class {
468
- /** 当前追踪的依赖 */
469
- dependencies = /* @__PURE__ */ new Set();
470
- /** 等待变更的 Promise */
471
- changePromise;
472
- /** 是否已销毁 */
473
- destroyed = false;
474
- /**
475
- * 追踪依赖
476
- * 由 ReactiveState.get() 调用
477
- */
478
- track(state) {
479
- if (!this.destroyed) this.dependencies.add(state);
480
- }
481
- /**
482
- * 通知变更
483
- * 由 ReactiveState.set() 调用
484
- */
485
- notifyChange() {
486
- if (!this.destroyed && this.changePromise) this.changePromise.resolve();
487
- }
488
- /**
489
- * 运行响应式任务流
490
- * 每次依赖变更时重新执行任务并 yield 结果
491
- *
492
- * @param task 要执行的异步任务
493
- * @param signal 用于取消的 AbortSignal
494
- */
495
- async *stream(task, signal) {
496
- try {
497
- while (!signal?.aborted && !this.destroyed) {
498
- this.clearDependencies();
499
- this.changePromise = createPromiseWithResolvers();
500
- yield await contextStorage.run(this, task);
501
- if (this.dependencies.size === 0) break;
502
- await Promise.race([this.changePromise.promise, signal ? this.waitForAbort(signal) : new Promise(() => {})]);
503
- if (signal?.aborted) break;
504
- }
505
- } finally {
506
- this.destroy();
507
- }
508
- }
509
- /**
510
- * 执行一次任务(非响应式)
511
- * 用于初始数据获取
512
- */
513
- async runOnce(task) {
514
- return contextStorage.run(this, task);
515
- }
516
- /**
517
- * 清理依赖
518
- */
519
- clearDependencies() {
520
- for (const state of this.dependencies) state.unsubscribe(this);
521
- this.dependencies.clear();
522
- }
523
- /**
524
- * 销毁上下文
525
- * @param reason 可选的销毁原因,如果提供则 reject changePromise
526
- */
527
- destroy(reason) {
528
- this.destroyed = true;
529
- this.clearDependencies();
530
- if (reason && this.changePromise) this.changePromise.reject(reason);
531
- this.changePromise = void 0;
532
- }
533
- /**
534
- * 等待 AbortSignal
535
- */
536
- waitForAbort(signal) {
537
- return new Promise((_, reject) => {
538
- if (signal.aborted) {
539
- reject(new DOMException("Aborted", "AbortError"));
540
- return;
541
- }
542
- signal.addEventListener("abort", () => {
543
- reject(new DOMException("Aborted", "AbortError"));
544
- });
545
- });
546
- }
547
- };
548
-
549
303
  //#endregion
550
304
  //#region src/reactive-fs/path-realpath.ts
551
305
  /**
@@ -591,6 +345,10 @@ const DEFAULT_IGNORE = [
591
345
  const RECOVERY_INTERVAL_MS = 3e3;
592
346
  /** 路径语义检查间隔 (ms) */
593
347
  const PATH_LIVENESS_INTERVAL_MS = 3e3;
348
+ function isStrictAncestorPath(ancestorPath, candidatePath) {
349
+ const childPath = relative(ancestorPath, candidatePath);
350
+ return childPath !== "" && childPath !== ".." && !isAbsolute(childPath) && !childPath.startsWith(`..${sep}`);
351
+ }
594
352
  /**
595
353
  * 项目监听器
596
354
  *
@@ -780,6 +538,8 @@ var ProjectWatcher = class {
780
538
  sub.callback(matchedEvents);
781
539
  } catch (err) {
782
540
  console.error(`[ProjectWatcher] Callback error for ${sub.path}:`, err);
541
+ } finally {
542
+ sub.awaitingPathCreation = sub.watchAncestorsWhileMissing && !existsSync(sub.path);
783
543
  }
784
544
  }
785
545
  }
@@ -788,6 +548,7 @@ var ProjectWatcher = class {
788
548
  */
789
549
  matchPath(event, sub) {
790
550
  const eventPath = event.path;
551
+ if (sub.watchAncestorsWhileMissing && sub.awaitingPathCreation && isStrictAncestorPath(eventPath, sub.path)) return true;
791
552
  if (sub.watchChildren) return eventPath.startsWith(sub.path + "/") || eventPath === sub.path;
792
553
  else {
793
554
  const eventDir = dirname$1(eventPath);
@@ -812,6 +573,8 @@ var ProjectWatcher = class {
812
573
  this.pathSubscriptions.set(id, {
813
574
  path: normalizedPath,
814
575
  watchChildren: options.watchChildren ?? false,
576
+ watchAncestorsWhileMissing: options.watchAncestorsWhileMissing ?? false,
577
+ awaitingPathCreation: (options.watchAncestorsWhileMissing ?? false) && !existsSync(normalizedPath),
815
578
  callback
816
579
  });
817
580
  return () => {
@@ -1002,177 +765,265 @@ async function closeAllProjectWatchers() {
1002
765
  //#endregion
1003
766
  //#region src/reactive-fs/watcher-pool.ts
1004
767
  /**
1005
- * 获取路径的真实路径(解析符号链接)
768
+ * Orthogonal intents (updated 2026-07-28 Asia/Shanghai):
769
+ * 1. Own a reference-counted dynamic set of physical watcher roots, including missing logical roots
770
+ * through their nearest existing ancestor.
771
+ * 2. Bind shared path subscriptions to the deepest active containing root.
772
+ * 3. Rebind pending subscriptions when roots appear, disappear, or recover.
773
+ * 4. Expose aggregate and per-root runtime status for server diagnostics.
774
+ * 5. Keep missing-path ancestor settlement explicit in subscription identity and release all leases deterministically.
775
+ *
776
+ * Original request (2026-07-15): "响应式内核要观察 data home、Store roots 和 connected project roots。"
777
+ * Remote CI fixed point (2026-07-28): data-home Schema creation may arrive as an ancestor event on Linux.
1006
778
  */
779
+ const DEBOUNCE_MS = 100;
1007
780
  function getRealPath(path) {
1008
781
  return resolveRealPathThroughExistingAncestor(path);
1009
782
  }
1010
- /**
1011
- * 全局 ProjectWatcher 实例
1012
- * 通过 initWatcherPool 初始化
1013
- */
1014
- let globalProjectWatcher = null;
1015
- let globalProjectDir = null;
1016
- /** 默认防抖时间 (ms) */
1017
- const DEBOUNCE_MS = 100;
1018
- /** 路径订阅缓存 */
783
+ function getExistingWatcherRoot(rootPath) {
784
+ let currentPath = rootPath;
785
+ while (!existsSync(currentPath)) {
786
+ const parentPath = dirname$1(currentPath);
787
+ if (parentPath === currentPath) return currentPath;
788
+ currentPath = parentPath;
789
+ }
790
+ return currentPath;
791
+ }
792
+ function pathBelongsToRoot(path, rootPath) {
793
+ const relativePath = relative(rootPath, path);
794
+ return relativePath === "" || !relativePath.startsWith("..") && !isAbsolute(relativePath);
795
+ }
796
+ const watcherRoots = /* @__PURE__ */ new Map();
797
+ const closingWatcherRoots = /* @__PURE__ */ new Map();
798
+ const physicalWatcherReferences = /* @__PURE__ */ new Map();
799
+ const closingPhysicalWatchers = /* @__PURE__ */ new Map();
1019
800
  const subscriptionCache = /* @__PURE__ */ new Map();
1020
- /** 防抖定时器 */
1021
801
  const debounceTimers = /* @__PURE__ */ new Map();
1022
802
  const watcherRuntimeStatusListeners = /* @__PURE__ */ new Set();
1023
- let releaseProjectWatcherRuntimeSubscription = null;
1024
803
  function emitWatcherRuntimeStatus() {
1025
804
  const status = getWatcherRuntimeStatus();
1026
805
  for (const listener of watcherRuntimeStatusListeners) listener(status);
1027
806
  }
1028
- function bindProjectWatcherRuntimeStatus() {
1029
- releaseProjectWatcherRuntimeSubscription?.();
1030
- releaseProjectWatcherRuntimeSubscription = null;
1031
- if (!globalProjectWatcher) {
1032
- emitWatcherRuntimeStatus();
1033
- return;
807
+ function findWatcherRoot(path) {
808
+ let selected = null;
809
+ for (const root of watcherRoots.values()) if (root.referenceCount > 0 && root.watcher.isInitialized && pathBelongsToRoot(path, root.rootPath) && (!selected || root.rootPath.length > selected.rootPath.length)) selected = root;
810
+ return selected;
811
+ }
812
+ function dispatchSubscription(cacheKey, subscription) {
813
+ const existingTimer = debounceTimers.get(cacheKey);
814
+ if (existingTimer) clearTimeout(existingTimer);
815
+ const timer = setTimeout(() => {
816
+ debounceTimers.delete(cacheKey);
817
+ const current = subscriptionCache.get(cacheKey);
818
+ if (current !== subscription) return;
819
+ for (const callback of current.callbacks) try {
820
+ callback();
821
+ } catch (error) {
822
+ console.error(`[watcher-pool] Callback error for ${subscription.path}:`, error);
823
+ }
824
+ }, subscription.debounceMs);
825
+ debounceTimers.set(cacheKey, timer);
826
+ }
827
+ function rebindSubscription(cacheKey, subscription) {
828
+ const root = findWatcherRoot(subscription.path);
829
+ if (subscription.rootPath === root?.rootPath) return;
830
+ subscription.unsubscribe?.();
831
+ subscription.unsubscribe = null;
832
+ subscription.rootPath = null;
833
+ if (!root) return;
834
+ try {
835
+ subscription.unsubscribe = root.watcher.subscribeSync(subscription.path, () => dispatchSubscription(cacheKey, subscription), {
836
+ watchChildren: subscription.recursive,
837
+ watchAncestorsWhileMissing: subscription.watchAncestorsWhileMissing
838
+ });
839
+ subscription.rootPath = root.rootPath;
840
+ } catch (error) {
841
+ subscription.onError?.();
842
+ console.error(`[watcher-pool] Failed to bind ${subscription.path}:`, error);
1034
843
  }
844
+ }
845
+ function rebindAllSubscriptions() {
846
+ for (const [cacheKey, subscription] of subscriptionCache) rebindSubscription(cacheKey, subscription);
847
+ }
848
+ function createWatcherRoot(rootPath) {
849
+ const physicalRootPath = getExistingWatcherRoot(rootPath);
850
+ const existingPhysical = physicalWatcherReferences.get(physicalRootPath);
851
+ const watcher = existingPhysical?.watcher ?? getProjectWatcher(physicalRootPath);
852
+ if (existingPhysical) existingPhysical.referenceCount += 1;
853
+ else physicalWatcherReferences.set(physicalRootPath, {
854
+ watcher,
855
+ referenceCount: 1
856
+ });
857
+ const record = {
858
+ rootPath,
859
+ physicalRootPath,
860
+ watcher,
861
+ referenceCount: 0,
862
+ ready: Promise.resolve(),
863
+ releaseRuntimeStatus: () => {}
864
+ };
865
+ watcherRoots.set(rootPath, record);
1035
866
  const forward = () => {
867
+ rebindAllSubscriptions();
1036
868
  emitWatcherRuntimeStatus();
1037
869
  };
1038
- releaseProjectWatcherRuntimeSubscription = globalProjectWatcher.subscribeRuntimeStatus(forward, { emitCurrent: false });
870
+ record.releaseRuntimeStatus = watcher.subscribeRuntimeStatus(forward, { emitCurrent: false });
871
+ record.ready = watcher.init().then(() => {
872
+ rebindAllSubscriptions();
873
+ emitWatcherRuntimeStatus();
874
+ });
875
+ return record;
876
+ }
877
+ async function releaseWatcherRootReference(record) {
878
+ if (watcherRoots.get(record.rootPath) !== record || record.referenceCount === 0) return;
879
+ record.referenceCount -= 1;
880
+ if (record.referenceCount > 0) {
881
+ emitWatcherRuntimeStatus();
882
+ return;
883
+ }
884
+ watcherRoots.delete(record.rootPath);
885
+ record.releaseRuntimeStatus();
886
+ rebindAllSubscriptions();
887
+ const physical = physicalWatcherReferences.get(record.physicalRootPath);
888
+ if (!physical || physical.referenceCount <= 0) {
889
+ emitWatcherRuntimeStatus();
890
+ return;
891
+ }
892
+ physical.referenceCount -= 1;
893
+ if (physical.referenceCount > 0) {
894
+ emitWatcherRuntimeStatus();
895
+ return;
896
+ }
897
+ physicalWatcherReferences.delete(record.physicalRootPath);
898
+ const closing = record.watcher.close().finally(() => {
899
+ if (closingWatcherRoots.get(record.rootPath) === closing) closingWatcherRoots.delete(record.rootPath);
900
+ if (closingPhysicalWatchers.get(record.physicalRootPath) === closing) closingPhysicalWatchers.delete(record.physicalRootPath);
901
+ emitWatcherRuntimeStatus();
902
+ });
903
+ closingWatcherRoots.set(record.rootPath, closing);
904
+ closingPhysicalWatchers.set(record.physicalRootPath, closing);
1039
905
  emitWatcherRuntimeStatus();
906
+ await closing;
1040
907
  }
1041
- /**
1042
- * 初始化 watcher pool
1043
- *
1044
- * 必须在使用 acquireWatcher 之前调用。
1045
- * 通常由 server 在启动时调用。
1046
- *
1047
- * @param projectDir 项目根目录
1048
- */
1049
- async function initWatcherPool(projectDir) {
1050
- const normalizedDir = getRealPath(projectDir);
1051
- if (globalProjectWatcher && globalProjectDir === normalizedDir) return;
1052
- if (globalProjectWatcher) {
1053
- releaseProjectWatcherRuntimeSubscription?.();
1054
- releaseProjectWatcherRuntimeSubscription = null;
1055
- await globalProjectWatcher.close();
1056
- }
1057
- globalProjectDir = normalizedDir;
1058
- globalProjectWatcher = getProjectWatcher(normalizedDir);
1059
- bindProjectWatcherRuntimeStatus();
1060
- await globalProjectWatcher.init();
908
+ /** Acquire one physical observation root and release it when the returned lease is no longer used. */
909
+ async function acquireWatcherRoot(rootPath) {
910
+ const normalizedRoot = getRealPath(rootPath);
911
+ const physicalRootPath = getExistingWatcherRoot(normalizedRoot);
912
+ await Promise.all([closingWatcherRoots.get(normalizedRoot), closingPhysicalWatchers.get(physicalRootPath)]);
913
+ const record = watcherRoots.get(normalizedRoot) ?? createWatcherRoot(normalizedRoot);
914
+ record.referenceCount += 1;
915
+ emitWatcherRuntimeStatus();
916
+ try {
917
+ await record.ready;
918
+ } catch (error) {
919
+ await releaseWatcherRootReference(record);
920
+ throw error;
921
+ }
922
+ let released = false;
923
+ return async () => {
924
+ if (released) return;
925
+ released = true;
926
+ await releaseWatcherRootReference(record);
927
+ };
1061
928
  }
1062
929
  /**
1063
- * 获取或创建文件/目录监听器
930
+ * Acquire a shared path subscription.
1064
931
  *
1065
- * 特性:
1066
- * - 使用 @parcel/watcher 监听项目根目录
1067
- * - 自动处理新创建的目录(解决 init 后无法监听的问题)
1068
- * - 同一路径共享订阅
1069
- * - 引用计数管理生命周期
1070
- * - 内置防抖机制
1071
- *
1072
- * @param path 要监听的路径
1073
- * @param onChange 变更回调
1074
- * @param options 监听选项
1075
- * @returns 释放函数,调用后取消订阅
932
+ * The subscription remains pending when no active root contains the path and binds automatically
933
+ * after a matching root is acquired. This keeps static reads inert without making later live reads
934
+ * permanently lose observation.
1076
935
  */
1077
936
  function acquireWatcher(path, onChange, options = {}) {
1078
- if (!globalProjectWatcher || !globalProjectWatcher.isInitialized) return () => {};
1079
937
  const normalizedPath = getRealPath(path);
1080
- const debounceMs = options.debounceMs ?? DEBOUNCE_MS;
1081
- const isRecursive = options.recursive ?? false;
1082
- const cacheKey = `${normalizedPath}:${isRecursive}`;
938
+ const recursive = options.recursive ?? false;
939
+ const watchAncestorsWhileMissing = options.watchAncestorsWhileMissing ?? false;
940
+ const cacheKey = `${normalizedPath}:${recursive}:${watchAncestorsWhileMissing}`;
1083
941
  let subscription = subscriptionCache.get(cacheKey);
1084
942
  if (!subscription) {
1085
- const unsubscribe = globalProjectWatcher.subscribeSync(normalizedPath, () => {
1086
- const existingTimer = debounceTimers.get(cacheKey);
1087
- if (existingTimer) clearTimeout(existingTimer);
1088
- const timer = setTimeout(() => {
1089
- debounceTimers.delete(cacheKey);
1090
- const currentSub = subscriptionCache.get(cacheKey);
1091
- if (currentSub) for (const cb of currentSub.callbacks) try {
1092
- cb();
1093
- } catch (err) {
1094
- console.error(`[watcher-pool] Callback error for ${normalizedPath}:`, err);
1095
- }
1096
- }, debounceMs);
1097
- debounceTimers.set(cacheKey, timer);
1098
- }, { watchChildren: isRecursive });
1099
943
  subscription = {
1100
944
  path: normalizedPath,
945
+ recursive,
946
+ watchAncestorsWhileMissing,
947
+ debounceMs: options.debounceMs ?? DEBOUNCE_MS,
1101
948
  callbacks: /* @__PURE__ */ new Set(),
1102
- unsubscribe,
949
+ rootPath: null,
950
+ unsubscribe: null,
1103
951
  onError: options.onError
1104
952
  };
1105
953
  subscriptionCache.set(cacheKey, subscription);
954
+ rebindSubscription(cacheKey, subscription);
1106
955
  }
1107
956
  subscription.callbacks.add(onChange);
957
+ let released = false;
1108
958
  return () => {
1109
- const currentSub = subscriptionCache.get(cacheKey);
1110
- if (!currentSub) return;
1111
- currentSub.callbacks.delete(onChange);
1112
- if (currentSub.callbacks.size === 0) {
1113
- currentSub.unsubscribe();
1114
- subscriptionCache.delete(cacheKey);
1115
- const timer = debounceTimers.get(cacheKey);
1116
- if (timer) {
1117
- clearTimeout(timer);
1118
- debounceTimers.delete(cacheKey);
1119
- }
959
+ if (released) return;
960
+ released = true;
961
+ const current = subscriptionCache.get(cacheKey);
962
+ if (!current) return;
963
+ current.callbacks.delete(onChange);
964
+ if (current.callbacks.size > 0) return;
965
+ current.unsubscribe?.();
966
+ subscriptionCache.delete(cacheKey);
967
+ const timer = debounceTimers.get(cacheKey);
968
+ if (timer) {
969
+ clearTimeout(timer);
970
+ debounceTimers.delete(cacheKey);
1120
971
  }
1121
972
  };
1122
973
  }
1123
- /**
1124
- * 获取当前活跃的监听器数量(用于调试)
1125
- */
1126
974
  function getActiveWatcherCount() {
1127
975
  return subscriptionCache.size;
1128
976
  }
1129
- /**
1130
- * 关闭所有监听器(用于测试清理)
1131
- */
977
+ /** Close the entire process-level pool. Intended for runtime teardown and test isolation. */
1132
978
  async function closeAllWatchers() {
1133
- for (const [key, sub] of subscriptionCache) {
1134
- sub.unsubscribe();
1135
- const timer = debounceTimers.get(key);
979
+ for (const [cacheKey, subscription] of subscriptionCache) {
980
+ subscription.unsubscribe?.();
981
+ const timer = debounceTimers.get(cacheKey);
1136
982
  if (timer) clearTimeout(timer);
1137
983
  }
1138
984
  subscriptionCache.clear();
1139
985
  debounceTimers.clear();
1140
- if (globalProjectWatcher) {
1141
- releaseProjectWatcherRuntimeSubscription?.();
1142
- releaseProjectWatcherRuntimeSubscription = null;
1143
- await globalProjectWatcher.close();
1144
- globalProjectWatcher = null;
1145
- globalProjectDir = null;
1146
- }
986
+ const records = [...watcherRoots.values()];
987
+ watcherRoots.clear();
988
+ for (const record of records) {
989
+ record.referenceCount = 0;
990
+ record.releaseRuntimeStatus();
991
+ }
992
+ const physicalWatchers = [...physicalWatcherReferences.values()].map(({ watcher }) => watcher);
993
+ physicalWatcherReferences.clear();
994
+ await Promise.all([
995
+ ...physicalWatchers.map((watcher) => watcher.close()),
996
+ ...closingWatcherRoots.values(),
997
+ ...closingPhysicalWatchers.values()
998
+ ]);
999
+ closingWatcherRoots.clear();
1000
+ closingPhysicalWatchers.clear();
1147
1001
  emitWatcherRuntimeStatus();
1148
1002
  }
1149
- /**
1150
- * 检查 watcher pool 是否已初始化
1151
- */
1152
1003
  function isWatcherPoolInitialized() {
1153
- return globalProjectWatcher !== null && globalProjectWatcher.isInitialized;
1154
- }
1155
- /**
1156
- * 获取当前监听的项目目录
1157
- */
1158
- function getWatchedProjectDir() {
1159
- return globalProjectDir;
1004
+ return [...watcherRoots.values()].some((root) => root.watcher.isInitialized);
1160
1005
  }
1161
- /**
1162
- * 获取 watcher 运行时状态
1163
- */
1164
1006
  function getWatcherRuntimeStatus() {
1165
- if (!globalProjectWatcher) return null;
1166
- const runtime = globalProjectWatcher.runtimeStatus;
1007
+ const roots = [...watcherRoots.values()].filter((root) => root.referenceCount > 0).map((root) => {
1008
+ const runtime = root.watcher.runtimeStatus;
1009
+ return {
1010
+ rootPath: root.rootPath,
1011
+ referenceCount: root.referenceCount,
1012
+ initialized: root.watcher.isInitialized,
1013
+ subscriptionCount: root.watcher.subscriptionCount,
1014
+ generation: runtime.generation,
1015
+ reinitializeCount: runtime.reinitializeCount,
1016
+ lastReinitializeReason: runtime.lastReinitializeReason,
1017
+ reinitializeReasonCounts: runtime.reinitializeReasonCounts,
1018
+ projectResidency: runtime.projectResidency
1019
+ };
1020
+ }).sort((left, right) => left.rootPath.localeCompare(right.rootPath));
1021
+ if (roots.length === 0) return null;
1167
1022
  return {
1168
- projectDir: globalProjectDir,
1169
- initialized: globalProjectWatcher.isInitialized,
1170
- subscriptionCount: globalProjectWatcher.subscriptionCount,
1171
- generation: runtime.generation,
1172
- reinitializeCount: runtime.reinitializeCount,
1173
- lastReinitializeReason: runtime.lastReinitializeReason,
1174
- reinitializeReasonCounts: runtime.reinitializeReasonCounts,
1175
- projectResidency: runtime.projectResidency
1023
+ initialized: roots.some((root) => root.initialized),
1024
+ rootCount: roots.length,
1025
+ subscriptionCount: roots.reduce((count, root) => count + root.subscriptionCount, 0),
1026
+ roots
1176
1027
  };
1177
1028
  }
1178
1029
  function subscribeWatcherRuntimeStatus(listener, options = {}) {
@@ -1184,26 +1035,301 @@ function subscribeWatcherRuntimeStatus(listener, options = {}) {
1184
1035
  }
1185
1036
 
1186
1037
  //#endregion
1187
- //#region src/reactive-fs/reactive-fs.ts
1188
- /** 状态缓存:路径 -> ReactiveState */
1189
- const stateCache$1 = /* @__PURE__ */ new Map();
1190
- /** 监听器释放函数缓存 */
1191
- const releaseCache$1 = /* @__PURE__ */ new Map();
1192
- /** 缺失路径重校验定时器缓存 */
1193
- const missingPathPollCache = /* @__PURE__ */ new Map();
1194
- /** Native watcher can miss a create event on some CI filesystems; missing paths get a low-frequency fallback. */
1195
- const MISSING_PATH_POLL_MS = 1e3;
1196
- function stopMissingPathPoll(key) {
1197
- const timer = missingPathPollCache.get(key);
1198
- if (!timer) return;
1199
- clearInterval(timer);
1200
- missingPathPollCache.delete(key);
1201
- }
1202
- function ensureMissingPathPoll(key, poll) {
1203
- if (!isWatcherPoolInitialized() || missingPathPollCache.has(key)) return;
1204
- const timer = setInterval(() => {
1205
- poll();
1206
- }, MISSING_PATH_POLL_MS);
1038
+ //#region src/reactive-fs/observation-environment.ts
1039
+ /**
1040
+ * Orthogonal intents (created 2026-07-16 Asia/Shanghai):
1041
+ * 1. Own the dynamic observation-root set for one backend runtime environment.
1042
+ * 2. Reference-count repeated logical acquisition inside that environment.
1043
+ * 3. Share physical root watchers through the process-level watcher pool.
1044
+ * 4. Release every root deterministically during environment teardown.
1045
+ *
1046
+ * Original request (2026-07-15): "Reactive observation supports a reference-counted dynamic set of roots per runtime environment."
1047
+ */
1048
+ /** Runtime-environment owner for all roots observed by one backend process. */
1049
+ var ReactiveObservationEnvironment = class {
1050
+ roots = /* @__PURE__ */ new Map();
1051
+ disposed = false;
1052
+ async acquireRoot(rootPath) {
1053
+ if (this.disposed) throw new Error("Cannot acquire an observation root after environment teardown.");
1054
+ const normalizedRoot = resolveRealPathThroughExistingAncestor(rootPath);
1055
+ let record = this.roots.get(normalizedRoot);
1056
+ if (!record) {
1057
+ record = {
1058
+ rootPath: normalizedRoot,
1059
+ referenceCount: 0,
1060
+ lease: acquireWatcherRoot(normalizedRoot)
1061
+ };
1062
+ this.roots.set(normalizedRoot, record);
1063
+ }
1064
+ record.referenceCount += 1;
1065
+ try {
1066
+ await record.lease;
1067
+ } catch (error) {
1068
+ record.referenceCount -= 1;
1069
+ if (record.referenceCount === 0 && this.roots.get(normalizedRoot) === record) this.roots.delete(normalizedRoot);
1070
+ throw error;
1071
+ }
1072
+ let released = false;
1073
+ return async () => {
1074
+ if (released) return;
1075
+ released = true;
1076
+ const current = this.roots.get(normalizedRoot);
1077
+ if (current !== record || current.referenceCount === 0) return;
1078
+ current.referenceCount -= 1;
1079
+ if (current.referenceCount > 0) return;
1080
+ this.roots.delete(normalizedRoot);
1081
+ await (await current.lease)();
1082
+ };
1083
+ }
1084
+ getRoots() {
1085
+ return [...this.roots.values()].map(({ rootPath, referenceCount }) => ({
1086
+ rootPath,
1087
+ referenceCount
1088
+ })).sort((left, right) => left.rootPath.localeCompare(right.rootPath));
1089
+ }
1090
+ async dispose() {
1091
+ if (this.disposed) return;
1092
+ this.disposed = true;
1093
+ const records = [...this.roots.values()];
1094
+ this.roots.clear();
1095
+ await Promise.all(records.map(async (record) => {
1096
+ await (await record.lease)();
1097
+ }));
1098
+ }
1099
+ };
1100
+
1101
+ //#endregion
1102
+ //#region src/reactive-fs/reactive-state.ts
1103
+ /**
1104
+ * 全局的 AsyncLocalStorage,用于在异步调用链中传递 ReactiveContext
1105
+ * 这是实现依赖收集的核心机制
1106
+ */
1107
+ const contextStorage = new AsyncLocalStorage();
1108
+ /**
1109
+ * 响应式状态类,类似 Signal.State
1110
+ *
1111
+ * 核心机制:
1112
+ * - get() 时自动注册到当前 ReactiveContext 的依赖列表
1113
+ * - set() 时如果值变化,通知所有依赖的 Context
1114
+ */
1115
+ var ReactiveState = class {
1116
+ currentValue;
1117
+ equals;
1118
+ /** 所有依赖此状态的 Context */
1119
+ subscribers = /* @__PURE__ */ new Set();
1120
+ constructor(initialValue, options) {
1121
+ this.currentValue = initialValue;
1122
+ this.equals = options?.equals ?? ((a, b) => a === b);
1123
+ }
1124
+ /**
1125
+ * 获取当前值
1126
+ * 如果在 ReactiveContext 中调用,会自动注册依赖
1127
+ */
1128
+ get() {
1129
+ const context = contextStorage.getStore();
1130
+ if (context) {
1131
+ context.track(this);
1132
+ this.subscribers.add(context);
1133
+ }
1134
+ return this.currentValue;
1135
+ }
1136
+ /**
1137
+ * 设置新值
1138
+ * 如果值变化,通知所有订阅者
1139
+ * @returns 是否发生了变化
1140
+ */
1141
+ set(newValue) {
1142
+ if (this.equals(this.currentValue, newValue)) return false;
1143
+ this.currentValue = newValue;
1144
+ for (const context of this.subscribers) context.notifyChange();
1145
+ return true;
1146
+ }
1147
+ /**
1148
+ * 取消订阅
1149
+ * 当 Context 销毁时调用
1150
+ */
1151
+ unsubscribe(context) {
1152
+ this.subscribers.delete(context);
1153
+ }
1154
+ /**
1155
+ * 获取当前订阅者数量(用于调试)
1156
+ */
1157
+ get subscriberCount() {
1158
+ return this.subscribers.size;
1159
+ }
1160
+ };
1161
+
1162
+ //#endregion
1163
+ //#region src/reactive-fs/reactive-context.ts
1164
+ /**
1165
+ * Orthogonal intents (updated 2026-07-26 Asia/Shanghai):
1166
+ * 1. Collect reactive dependencies for asynchronous projections.
1167
+ * 2. Coalesce dependency changes into replacement task executions.
1168
+ * 3. Expose opt-in recompute lifecycle without changing yielded data.
1169
+ *
1170
+ * Original request (2026-07-22): "整个过程中,几乎都在 Loading。"
1171
+ * Original request (2026-07-26): "真正基于文件、甚至是文件内容结构的变更去拉取更新。"
1172
+ */
1173
+ function createPromiseWithResolvers() {
1174
+ let resolve$1;
1175
+ let reject;
1176
+ return {
1177
+ promise: new Promise((res, rej) => {
1178
+ resolve$1 = res;
1179
+ reject = rej;
1180
+ }),
1181
+ resolve: resolve$1,
1182
+ reject
1183
+ };
1184
+ }
1185
+ /**
1186
+ * 响应式上下文,管理依赖收集和变更通知
1187
+ *
1188
+ * 核心机制:
1189
+ * - 在 stream() 中执行任务时,通过 AsyncLocalStorage 传递 this
1190
+ * - 任务中的所有 ReactiveState.get() 调用都会自动注册依赖
1191
+ * - 当任何依赖变更时,重新执行任务并 yield 新结果
1192
+ */
1193
+ var ReactiveContext = class {
1194
+ /** 当前追踪的依赖 */
1195
+ dependencies = /* @__PURE__ */ new Set();
1196
+ /** 等待变更的 Promise */
1197
+ changePromise;
1198
+ /** 是否已销毁 */
1199
+ destroyed = false;
1200
+ /** 当前任务执行期间是否已有依赖变化,使其结果不再可提交 */
1201
+ changePending = false;
1202
+ /**
1203
+ * 追踪依赖
1204
+ * 由 ReactiveState.get() 调用
1205
+ */
1206
+ track(state) {
1207
+ if (!this.destroyed) this.dependencies.add(state);
1208
+ }
1209
+ /**
1210
+ * 通知变更
1211
+ * 由 ReactiveState.set() 调用
1212
+ */
1213
+ notifyChange() {
1214
+ if (this.destroyed) return;
1215
+ this.changePending = true;
1216
+ this.changePromise?.resolve();
1217
+ }
1218
+ /**
1219
+ * 运行响应式任务流
1220
+ * 每次依赖变更时重新执行任务并 yield 结果
1221
+ *
1222
+ * @param task 要执行的异步任务
1223
+ * @param signal 用于取消的 AbortSignal
1224
+ * @param observer 可选的依赖重算生命周期观察器
1225
+ */
1226
+ async *stream(task, signal, observer) {
1227
+ let abortPromise = null;
1228
+ try {
1229
+ while (!signal?.aborted && !this.destroyed) {
1230
+ this.clearDependencies();
1231
+ this.changePending = false;
1232
+ this.changePromise = createPromiseWithResolvers();
1233
+ let result;
1234
+ try {
1235
+ result = await contextStorage.run(this, task);
1236
+ } catch (error) {
1237
+ if (this.changePending) {
1238
+ if (signal?.aborted) break;
1239
+ observer?.onRecomputeStarted();
1240
+ continue;
1241
+ }
1242
+ throw error;
1243
+ }
1244
+ if (signal?.aborted) break;
1245
+ if (this.changePending) {
1246
+ if (signal?.aborted) break;
1247
+ observer?.onRecomputeStarted();
1248
+ continue;
1249
+ }
1250
+ yield result;
1251
+ if (this.dependencies.size === 0) break;
1252
+ abortPromise ??= signal ? this.waitForAbort(signal) : new Promise(() => {});
1253
+ await Promise.race([this.changePromise.promise, abortPromise]);
1254
+ if (signal?.aborted) break;
1255
+ observer?.onRecomputeStarted();
1256
+ }
1257
+ } finally {
1258
+ this.destroy();
1259
+ }
1260
+ }
1261
+ /**
1262
+ * 执行一次任务(非响应式)
1263
+ * 用于初始数据获取
1264
+ */
1265
+ async runOnce(task) {
1266
+ return contextStorage.run(this, task);
1267
+ }
1268
+ /**
1269
+ * 清理依赖
1270
+ */
1271
+ clearDependencies() {
1272
+ for (const state of this.dependencies) state.unsubscribe(this);
1273
+ this.dependencies.clear();
1274
+ }
1275
+ /**
1276
+ * 销毁上下文
1277
+ * @param reason 可选的销毁原因,如果提供则 reject changePromise
1278
+ */
1279
+ destroy(reason) {
1280
+ this.destroyed = true;
1281
+ this.clearDependencies();
1282
+ if (reason && this.changePromise) this.changePromise.reject(reason);
1283
+ this.changePromise = void 0;
1284
+ }
1285
+ /**
1286
+ * 等待 AbortSignal
1287
+ */
1288
+ waitForAbort(signal) {
1289
+ return new Promise((_, reject) => {
1290
+ if (signal.aborted) {
1291
+ reject(new DOMException("Aborted", "AbortError"));
1292
+ return;
1293
+ }
1294
+ signal.addEventListener("abort", () => {
1295
+ reject(new DOMException("Aborted", "AbortError"));
1296
+ });
1297
+ });
1298
+ }
1299
+ };
1300
+
1301
+ //#endregion
1302
+ //#region src/reactive-fs/reactive-fs.ts
1303
+ /**
1304
+ * Orthogonal intents (updated 2026-07-16 Asia/Shanghai):
1305
+ * 1. Cache reactive file, directory, existence, and stat reads.
1306
+ * 2. Bind cached reads to shared watcher roots and missing-path fallback checks.
1307
+ * 3. Settle overlapping cached projections as one coherent post-mutation snapshot.
1308
+ * 4. Release watcher, polling, state, and refresh registrations together.
1309
+ *
1310
+ * Original request (2026-07-16): "direct filesystem mutations update reactive state before returning."
1311
+ */
1312
+ /** 状态缓存:路径 -> ReactiveState */
1313
+ const stateCache$1 = /* @__PURE__ */ new Map();
1314
+ /** Cache refreshers retained so owned writes can settle projections without watcher timing. */
1315
+ const refreshCache = /* @__PURE__ */ new Map();
1316
+ /** 监听器释放函数缓存 */
1317
+ const releaseCache$1 = /* @__PURE__ */ new Map();
1318
+ /** 缺失路径重校验定时器缓存 */
1319
+ const missingPathPollCache = /* @__PURE__ */ new Map();
1320
+ /** Native watcher can miss a create event on some CI filesystems; missing paths get a low-frequency fallback. */
1321
+ const MISSING_PATH_POLL_MS = 1e3;
1322
+ function stopMissingPathPoll(key) {
1323
+ const timer = missingPathPollCache.get(key);
1324
+ if (!timer) return;
1325
+ clearInterval(timer);
1326
+ missingPathPollCache.delete(key);
1327
+ }
1328
+ function ensureMissingPathPoll(key, poll) {
1329
+ if (!isWatcherPoolInitialized() || missingPathPollCache.has(key)) return;
1330
+ const timer = setInterval(() => {
1331
+ poll();
1332
+ }, MISSING_PATH_POLL_MS);
1207
1333
  timer.unref?.();
1208
1334
  missingPathPollCache.set(key, timer);
1209
1335
  }
@@ -1224,7 +1350,7 @@ async function reactiveReadFile(filepath) {
1224
1350
  const key = `file:${normalizedPath}`;
1225
1351
  const getValue = async () => {
1226
1352
  try {
1227
- return await readFile$1(normalizedPath, "utf-8");
1353
+ return await readFile(normalizedPath, "utf-8");
1228
1354
  } catch {
1229
1355
  return null;
1230
1356
  }
@@ -1234,16 +1360,28 @@ async function reactiveReadFile(filepath) {
1234
1360
  const initialValue = await getValue();
1235
1361
  state = new ReactiveState(initialValue);
1236
1362
  stateCache$1.set(key, state);
1237
- const refresh = async () => {
1363
+ const prepareRefresh = async () => {
1238
1364
  const newValue = await getValue();
1239
- state.set(newValue);
1240
- if (newValue === null) ensureMissingPathPoll(key, refresh);
1241
- else stopMissingPathPoll(key);
1365
+ return () => {
1366
+ state.set(newValue);
1367
+ if (newValue === null) ensureMissingPathPoll(key, refresh);
1368
+ else stopMissingPathPoll(key);
1369
+ };
1370
+ };
1371
+ const refresh = async () => {
1372
+ (await prepareRefresh())();
1242
1373
  };
1374
+ refreshCache.set(key, {
1375
+ kind: "file",
1376
+ path: normalizedPath,
1377
+ prepareRefresh,
1378
+ refresh
1379
+ });
1243
1380
  if (initialValue === null) ensureMissingPathPoll(key, refresh);
1244
1381
  const release = acquireWatcher(dirname$1(normalizedPath), refresh, { onError: () => {
1245
1382
  stopMissingPathPoll(key);
1246
1383
  stateCache$1.delete(key);
1384
+ refreshCache.delete(key);
1247
1385
  releaseCache$1.delete(key);
1248
1386
  } });
1249
1387
  releaseCache$1.set(key, release);
@@ -1259,6 +1397,31 @@ function updateReactiveFileCache(filepath, content) {
1259
1397
  const key = `file:${resolve(filepath)}`;
1260
1398
  stateCache$1.get(key)?.set(content);
1261
1399
  }
1400
+ function containsPath(rootPath, candidatePath) {
1401
+ const childPath = relative(rootPath, candidatePath);
1402
+ return childPath === "" || !isAbsolute(childPath) && !childPath.startsWith(`..${sep}`) && childPath !== "..";
1403
+ }
1404
+ function pathsOverlap(firstPath, secondPath) {
1405
+ return containsPath(firstPath, secondPath) || containsPath(secondPath, firstPath);
1406
+ }
1407
+ async function settleReactivePathMutationInternal(path, exactFileContent) {
1408
+ const normalizedPath = resolve(path);
1409
+ const registrations = [...refreshCache.values()].filter((registration) => pathsOverlap(registration.path, normalizedPath));
1410
+ const commits = await Promise.all(registrations.flatMap((registration) => exactFileContent !== void 0 && registration.kind === "file" && registration.path === normalizedPath ? [] : [registration.prepareRefresh()]));
1411
+ for (const commit of commits) commit();
1412
+ if (exactFileContent !== void 0) updateReactiveFileCache(normalizedPath, exactFileContent);
1413
+ }
1414
+ /** Re-read and atomically publish every cached projection overlapping one committed path mutation. */
1415
+ async function settleReactivePathMutation(path) {
1416
+ await settleReactivePathMutationInternal(path);
1417
+ }
1418
+ /**
1419
+ * Publish a successful file write to every cached projection whose value can change.
1420
+ * File content is set from the committed bytes; directory/existence/stat views re-read disk.
1421
+ */
1422
+ async function settleReactiveFileWrite(filepath, content) {
1423
+ await settleReactivePathMutationInternal(filepath, content);
1424
+ }
1262
1425
  /**
1263
1426
  * 响应式读取目录内容
1264
1427
  *
@@ -1292,16 +1455,29 @@ async function reactiveReadDir(dirpath, options = {}) {
1292
1455
  if (!state) {
1293
1456
  state = new ReactiveState(await getValue(), { equals: (a, b) => a.length === b.length && a.every((v, i) => v === b[i]) });
1294
1457
  stateCache$1.set(key, state);
1295
- const release = acquireWatcher(normalizedPath, async () => {
1458
+ const prepareRefresh = async () => {
1296
1459
  const newValue = await getValue();
1297
- state.set(newValue);
1298
- }, {
1460
+ return () => {
1461
+ state.set(newValue);
1462
+ };
1463
+ };
1464
+ const refresh = async () => {
1465
+ (await prepareRefresh())();
1466
+ };
1467
+ const release = acquireWatcher(normalizedPath, refresh, {
1299
1468
  recursive: true,
1300
1469
  onError: () => {
1301
1470
  stateCache$1.delete(key);
1471
+ refreshCache.delete(key);
1302
1472
  releaseCache$1.delete(key);
1303
1473
  }
1304
1474
  });
1475
+ refreshCache.set(key, {
1476
+ kind: "directory",
1477
+ path: normalizedPath,
1478
+ prepareRefresh,
1479
+ refresh
1480
+ });
1305
1481
  releaseCache$1.set(key, release);
1306
1482
  }
1307
1483
  return state.get();
@@ -1328,16 +1504,28 @@ async function reactiveExists(path) {
1328
1504
  const initialValue = await getValue();
1329
1505
  state = new ReactiveState(initialValue);
1330
1506
  stateCache$1.set(key, state);
1331
- const refresh = async () => {
1507
+ const prepareRefresh = async () => {
1332
1508
  const newValue = await getValue();
1333
- state.set(newValue);
1334
- if (newValue) stopMissingPathPoll(key);
1335
- else ensureMissingPathPoll(key, refresh);
1509
+ return () => {
1510
+ state.set(newValue);
1511
+ if (newValue) stopMissingPathPoll(key);
1512
+ else ensureMissingPathPoll(key, refresh);
1513
+ };
1514
+ };
1515
+ const refresh = async () => {
1516
+ (await prepareRefresh())();
1336
1517
  };
1518
+ refreshCache.set(key, {
1519
+ kind: "exists",
1520
+ path: normalizedPath,
1521
+ prepareRefresh,
1522
+ refresh
1523
+ });
1337
1524
  if (!initialValue) ensureMissingPathPoll(key, refresh);
1338
1525
  const release = acquireWatcher(dirname$1(normalizedPath), refresh, { onError: () => {
1339
1526
  stopMissingPathPoll(key);
1340
1527
  stateCache$1.delete(key);
1528
+ refreshCache.delete(key);
1341
1529
  releaseCache$1.delete(key);
1342
1530
  } });
1343
1531
  releaseCache$1.set(key, release);
@@ -1375,16 +1563,28 @@ async function reactiveStat(path) {
1375
1563
  return a.isDirectory === b.isDirectory && a.isFile === b.isFile && a.mtime === b.mtime && a.birthtime === b.birthtime;
1376
1564
  } });
1377
1565
  stateCache$1.set(key, state);
1378
- const refresh = async () => {
1566
+ const prepareRefresh = async () => {
1379
1567
  const newValue = await getValue();
1380
- state.set(newValue);
1381
- if (newValue === null) ensureMissingPathPoll(key, refresh);
1382
- else stopMissingPathPoll(key);
1568
+ return () => {
1569
+ state.set(newValue);
1570
+ if (newValue === null) ensureMissingPathPoll(key, refresh);
1571
+ else stopMissingPathPoll(key);
1572
+ };
1383
1573
  };
1574
+ const refresh = async () => {
1575
+ (await prepareRefresh())();
1576
+ };
1577
+ refreshCache.set(key, {
1578
+ kind: "stat",
1579
+ path: normalizedPath,
1580
+ prepareRefresh,
1581
+ refresh
1582
+ });
1384
1583
  if (initialValue === null) ensureMissingPathPoll(key, refresh);
1385
1584
  const release = acquireWatcher(dirname$1(normalizedPath), refresh, { onError: () => {
1386
1585
  stopMissingPathPoll(key);
1387
1586
  stateCache$1.delete(key);
1587
+ refreshCache.delete(key);
1388
1588
  releaseCache$1.delete(key);
1389
1589
  } });
1390
1590
  releaseCache$1.set(key, release);
@@ -1392,30 +1592,154 @@ async function reactiveStat(path) {
1392
1592
  return state.get();
1393
1593
  }
1394
1594
  /**
1395
- * 清除指定路径的缓存(用于测试)
1595
+ * 清除指定路径的缓存(用于测试)
1596
+ */
1597
+ function clearCache(path) {
1598
+ if (path) {
1599
+ const normalizedPath = resolve(path);
1600
+ for (const [key, release] of releaseCache$1) if (key.includes(normalizedPath)) {
1601
+ release();
1602
+ stopMissingPathPoll(key);
1603
+ releaseCache$1.delete(key);
1604
+ stateCache$1.delete(key);
1605
+ refreshCache.delete(key);
1606
+ }
1607
+ } else {
1608
+ for (const release of releaseCache$1.values()) release();
1609
+ releaseCache$1.clear();
1610
+ for (const timer of missingPathPollCache.values()) clearInterval(timer);
1611
+ missingPathPollCache.clear();
1612
+ stateCache$1.clear();
1613
+ refreshCache.clear();
1614
+ }
1615
+ }
1616
+ /**
1617
+ * 获取缓存大小(用于调试)
1618
+ */
1619
+ function getCacheSize() {
1620
+ return stateCache$1.size;
1621
+ }
1622
+
1623
+ //#endregion
1624
+ //#region src/physical-reactive-file-writer.ts
1625
+ /**
1626
+ * Orthogonal intents (updated 2026-07-16 Asia/Shanghai):
1627
+ * 1. Own lexical and existing-ancestor physical confinement for root-scoped mutations.
1628
+ * 2. Execute UTF-8 write, directory-create, remove, and guarded external mutations.
1629
+ * 3. Settle overlapping reactive projections before returning any terminal outcome.
1630
+ * 4. Keep TOCTOU and hard-link alias limitations explicit at the mutation seam.
1631
+ *
1632
+ * Original request (2026-07-16): "建立唯一的 physical/reactive entity-write owner。"
1633
+ * Original request (2026-07-16): "Schema/Template mutations must reject symlink escape and settle reactive projections before success."
1634
+ */
1635
+ function isMissingPath(error) {
1636
+ return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
1637
+ }
1638
+ function ensureContained(rootPath, candidatePath, boundary) {
1639
+ const childPath = relative(rootPath, candidatePath);
1640
+ if (childPath === ".." || childPath.startsWith(`..${sep}`) || isAbsolute(childPath)) throw new Error(`Mutation target escaped the ${boundary} root.`);
1641
+ }
1642
+ async function readLstat(path) {
1643
+ try {
1644
+ return await lstat(path);
1645
+ } catch (error) {
1646
+ if (isMissingPath(error)) return null;
1647
+ throw error;
1648
+ }
1649
+ }
1650
+ async function assertNearestExistingAncestor(lexicalRoot, physicalRoot, candidatePath) {
1651
+ let currentPath = candidatePath;
1652
+ while (true) {
1653
+ if (await readLstat(currentPath)) {
1654
+ let physicalPath;
1655
+ try {
1656
+ physicalPath = await realpath(currentPath);
1657
+ } catch (error) {
1658
+ throw new Error("Mutation target could not be resolved inside the physical root.", { cause: error });
1659
+ }
1660
+ ensureContained(physicalRoot, physicalPath, "physical");
1661
+ return;
1662
+ }
1663
+ if (currentPath === lexicalRoot) throw new Error("Mutation root no longer exists.");
1664
+ const parentPath = dirname$1(currentPath);
1665
+ ensureContained(lexicalRoot, parentPath, "lexical");
1666
+ currentPath = parentPath;
1667
+ }
1668
+ }
1669
+ async function resolveMutationTarget(input) {
1670
+ const normalizedRelativePath = input.relativePath.replace(/\\/g, "/");
1671
+ if (!normalizedRelativePath || normalizedRelativePath.includes("\0") || isAbsolute(normalizedRelativePath) || /^[A-Za-z]:\//.test(normalizedRelativePath) || normalizedRelativePath.split("/").includes("..")) throw new Error("Mutation target must be a non-empty root-relative path.");
1672
+ const lexicalRoot = resolve(input.rootPath);
1673
+ const targetPath = resolve(lexicalRoot, normalizedRelativePath);
1674
+ ensureContained(lexicalRoot, targetPath, "lexical");
1675
+ if (targetPath === lexicalRoot) throw new Error("Mutation target cannot be the ownership root.");
1676
+ return {
1677
+ lexicalRoot,
1678
+ physicalRoot: await realpath(lexicalRoot),
1679
+ targetPath
1680
+ };
1681
+ }
1682
+ async function assertMutationTarget(target) {
1683
+ await assertNearestExistingAncestor(target.lexicalRoot, target.physicalRoot, target.targetPath);
1684
+ if ((await readLstat(target.targetPath))?.isSymbolicLink()) throw new Error("Mutation target escaped the physical root through a symbolic link.");
1685
+ }
1686
+ async function runResolvedPhysicalReactivePathMutation(input, mutation, settle = settleReactivePathMutation) {
1687
+ const target = await resolveMutationTarget(input);
1688
+ await assertMutationTarget(target);
1689
+ const outcome = await mutation(target).then((value) => ({
1690
+ success: true,
1691
+ value
1692
+ }), (error) => ({
1693
+ success: false,
1694
+ error
1695
+ }));
1696
+ const finalizationFailures = [];
1697
+ await assertMutationTarget(target).catch((error) => finalizationFailures.push(error));
1698
+ await settle(target.targetPath).catch((error) => finalizationFailures.push(error));
1699
+ if (!outcome.success) {
1700
+ if (finalizationFailures.length > 0) throw new AggregateError([outcome.error, ...finalizationFailures], "Physical/reactive mutation and settlement failed.");
1701
+ throw outcome.error;
1702
+ }
1703
+ if (finalizationFailures.length > 0) throw new AggregateError(finalizationFailures, "Physical/reactive mutation settlement failed.");
1704
+ return outcome.value;
1705
+ }
1706
+ /**
1707
+ * Write one UTF-8 file inside an existing physical root and synchronously settle reactive readers.
1708
+ *
1709
+ * Existing ancestors and the final target are resolved immediately before the write. This rejects
1710
+ * observed symlink escapes but cannot make path checks race-free against concurrent filesystem
1711
+ * replacement on platforms without a directory-handle-relative write primitive.
1396
1712
  */
1397
- function clearCache(path) {
1398
- if (path) {
1399
- const normalizedPath = resolve(path);
1400
- for (const [key, release] of releaseCache$1) if (key.includes(normalizedPath)) {
1401
- release();
1402
- stopMissingPathPoll(key);
1403
- releaseCache$1.delete(key);
1404
- stateCache$1.delete(key);
1405
- }
1406
- } else {
1407
- for (const release of releaseCache$1.values()) release();
1408
- releaseCache$1.clear();
1409
- for (const timer of missingPathPollCache.values()) clearInterval(timer);
1410
- missingPathPollCache.clear();
1411
- stateCache$1.clear();
1412
- }
1713
+ async function writePhysicalReactiveFile(input) {
1714
+ await runResolvedPhysicalReactivePathMutation(input, async (target) => {
1715
+ await mkdir$1(dirname$1(target.targetPath), { recursive: true });
1716
+ await assertMutationTarget(target);
1717
+ await writeFile$1(target.targetPath, input.content, "utf8");
1718
+ }, (targetPath) => settleReactiveFileWrite(targetPath, input.content));
1719
+ }
1720
+ /** Create one root-confined directory and synchronously settle overlapping reactive readers. */
1721
+ async function createPhysicalReactiveDirectory(input) {
1722
+ await runResolvedPhysicalReactivePathMutation(input, async (target) => {
1723
+ await mkdir$1(target.targetPath, { recursive: true });
1724
+ });
1725
+ }
1726
+ /** Remove one root-confined file or directory and synchronously settle descendant projections. */
1727
+ async function removePhysicalReactivePath(input) {
1728
+ await runResolvedPhysicalReactivePathMutation(input, async (target) => {
1729
+ await rm(target.targetPath, {
1730
+ recursive: true,
1731
+ force: true
1732
+ });
1733
+ });
1413
1734
  }
1414
1735
  /**
1415
- * 获取缓存大小(用于调试)
1736
+ * Guard and settle a mutation performed by an external owner such as the OpenSpec CLI.
1737
+ *
1738
+ * The pre/post checks reject observed symlink escape, but cannot close replacement races between
1739
+ * checks and the external process. Hard-link aliases are likewise not identifiable from the path.
1416
1740
  */
1417
- function getCacheSize() {
1418
- return stateCache$1.size;
1741
+ async function runPhysicalReactivePathMutation(input, mutation) {
1742
+ return runResolvedPhysicalReactivePathMutation(input, () => mutation());
1419
1743
  }
1420
1744
 
1421
1745
  //#endregion
@@ -1501,6 +1825,16 @@ var Validator = class {
1501
1825
  //#endregion
1502
1826
  //#region src/adapter.ts
1503
1827
  /**
1828
+ * Orthogonal intents (updated 2026-07-23 Asia/Shanghai):
1829
+ * 1. Read and mutate one CLI-selected planning root through reactive filesystem APIs.
1830
+ * 2. Project Specs, Changes, Archives, and schema-neutral entity files.
1831
+ * 3. Keep tracked workflow tasks distinct from document checklist analytics.
1832
+ * 4. Preserve filesystem provenance and mutation boundaries for server consumers.
1833
+ * 5. Expose one bounded Change row projection for progressive list delivery without warming workflow details.
1834
+ *
1835
+ * Original request (2026-07-15): "Split formal tracked progress, document checklist statistics, and Apply instruction progress into non-interchangeable facts."
1836
+ */
1837
+ /**
1504
1838
  * OpenSpec filesystem adapter
1505
1839
  * Handles reading, writing, and managing OpenSpec files
1506
1840
  */
@@ -1563,29 +1897,33 @@ var OpenSpecAdapter = class {
1563
1897
  exclude: ["archive"]
1564
1898
  });
1565
1899
  }
1900
+ /** Read one active Change row without resolving workflow Status, Apply, or artifact instructions. */
1901
+ async readChangeMeta(id) {
1902
+ const canonicalId = requireCanonicalOpenSpecEntityId(id, "changeId");
1903
+ const changeDir = join(this.changesDir, canonicalId);
1904
+ const [change, taskProjection, timeInfo] = await Promise.all([
1905
+ this.readChange(canonicalId),
1906
+ this.readChangeTaskProjection(canonicalId),
1907
+ this.getFileTimeInfo(changeDir)
1908
+ ]);
1909
+ return {
1910
+ id: canonicalId,
1911
+ name: change?.name ?? canonicalId,
1912
+ trackedTaskProgress: taskProjection.trackedTaskProgress,
1913
+ documentChecklistSummary: taskProjection.documentChecklistSummary,
1914
+ createdAt: timeInfo?.createdAt ?? 0,
1915
+ updatedAt: timeInfo?.updatedAt ?? 0
1916
+ };
1917
+ }
1566
1918
  /**
1567
- * List changes with metadata (id, name, progress, and time info)
1919
+ * List changes with metadata, separated task projections, and time info.
1568
1920
  * Returns every change directory, including schema-specific layouts that
1569
1921
  * don't use proposal.md/tasks.md.
1570
1922
  * Sorted by updatedAt descending (most recent first)
1571
1923
  */
1572
1924
  async listChangesWithMeta() {
1573
1925
  const ids = await this.listChanges();
1574
- return (await Promise.all(ids.map(async (id) => {
1575
- const changeDir = join(this.changesDir, id);
1576
- const [change, taskProjection, timeInfo] = await Promise.all([
1577
- this.readChange(id),
1578
- this.readChangeTaskProjection(id),
1579
- this.getFileTimeInfo(changeDir)
1580
- ]);
1581
- return {
1582
- id,
1583
- name: change?.name ?? id,
1584
- progress: taskProjection.progress,
1585
- createdAt: timeInfo?.createdAt ?? 0,
1586
- updatedAt: timeInfo?.updatedAt ?? 0
1587
- };
1588
- }))).sort((a, b) => b.updatedAt - a.updatedAt);
1926
+ return (await Promise.all(ids.map((id) => this.readChangeMeta(id)))).sort((a, b) => b.updatedAt - a.updatedAt);
1589
1927
  }
1590
1928
  async listArchivedChanges() {
1591
1929
  return reactiveReadDir(this.archiveDir, { directoriesOnly: true });
@@ -1604,7 +1942,8 @@ var OpenSpecAdapter = class {
1604
1942
  return {
1605
1943
  id,
1606
1944
  name: id,
1607
- progress: taskProjection.progress,
1945
+ trackedTaskProgress: taskProjection.trackedTaskProgress,
1946
+ documentChecklistSummary: taskProjection.documentChecklistSummary,
1608
1947
  createdAt: timeInfo?.createdAt ?? 0,
1609
1948
  updatedAt: timeInfo?.updatedAt ?? 0
1610
1949
  };
@@ -1632,7 +1971,8 @@ var OpenSpecAdapter = class {
1632
1971
  }
1633
1972
  }
1634
1973
  async readSpecRaw(specId) {
1635
- return reactiveReadFile(join(this.specsDir, specId, "spec.md"));
1974
+ const canonicalSpecId = requireCanonicalOpenSpecEntityId(specId, "specId");
1975
+ return reactiveReadFile(join(this.specsDir, canonicalSpecId, "spec.md"));
1636
1976
  }
1637
1977
  async readChange(changeId) {
1638
1978
  try {
@@ -1647,28 +1987,33 @@ var OpenSpecAdapter = class {
1647
1987
  }
1648
1988
  }
1649
1989
  async readChangeFiles(changeId) {
1650
- const changeRoot = join(this.changesDir, changeId);
1990
+ const canonicalChangeId = requireCanonicalOpenSpecEntityId(changeId, "changeId");
1991
+ const changeRoot = join(this.changesDir, canonicalChangeId);
1651
1992
  return this.readFilesUnderRoot(changeRoot);
1652
1993
  }
1653
1994
  async readArchivedChangeFiles(changeId) {
1654
- const archiveRoot = join(this.archiveDir, changeId);
1995
+ const canonicalChangeId = requireCanonicalOpenSpecEntityId(changeId, "changeId");
1996
+ const archiveRoot = join(this.archiveDir, canonicalChangeId);
1655
1997
  return this.readFilesUnderRoot(archiveRoot);
1656
1998
  }
1657
1999
  async readChangeTaskProjection(changeId) {
1658
- return this.readEntityTaskProjection(join(this.changesDir, changeId));
2000
+ const canonicalChangeId = requireCanonicalOpenSpecEntityId(changeId, "changeId");
2001
+ return this.readEntityTaskProjection(join(this.changesDir, canonicalChangeId));
1659
2002
  }
1660
2003
  async readArchivedChangeTaskProjection(changeId) {
1661
- return this.readEntityTaskProjection(join(this.archiveDir, changeId));
2004
+ const canonicalChangeId = requireCanonicalOpenSpecEntityId(changeId, "changeId");
2005
+ return this.readEntityTaskProjection(join(this.archiveDir, canonicalChangeId));
1662
2006
  }
1663
2007
  async readEntityDetail(stage, id, options = {}) {
1664
- const root = stage === "change" ? join(this.changesDir, id) : join(this.archiveDir, id);
2008
+ const canonicalId = requireCanonicalOpenSpecEntityId(id, "changeId");
2009
+ const root = stage === "change" ? join(this.changesDir, canonicalId) : join(this.archiveDir, canonicalId);
1665
2010
  const files = await this.readFilesUnderRoot(root);
1666
2011
  if (files.length === 0) {
1667
2012
  if (!(await reactiveStat(root))?.isDirectory) return null;
1668
2013
  }
1669
2014
  return buildOpsxEntityDetail({
1670
2015
  stage,
1671
- id,
2016
+ id: canonicalId,
1672
2017
  files,
1673
2018
  schemas: options.schemas,
1674
2019
  schemaDiagnostics: options.schemaDiagnostics
@@ -1722,13 +2067,14 @@ var OpenSpecAdapter = class {
1722
2067
  async readEntityTaskProjection(root) {
1723
2068
  const files = await this.readFilesUnderRoot(root);
1724
2069
  const metadata = parseOpsxEntityMetadata(files.find((file) => file.type === "file" && file.path === ".openspec.yaml")?.content ?? null);
1725
- return projectTasksFromMarkdownFiles(files, {
2070
+ return projectTaskProjectionsFromMarkdownFiles(files, {
1726
2071
  schemaDetail: metadata.schemaName ? await this.readProjectSchemaDetail(metadata.schemaName) : null,
1727
2072
  hasSchemaMetadata: Boolean(metadata.schemaName)
1728
2073
  });
1729
2074
  }
1730
2075
  async readChangeRaw(changeId) {
1731
- const changeDir = join(this.changesDir, changeId);
2076
+ const canonicalChangeId = requireCanonicalOpenSpecEntityId(changeId, "changeId");
2077
+ const changeDir = join(this.changesDir, canonicalChangeId);
1732
2078
  const proposalPath = join(changeDir, "proposal.md");
1733
2079
  const tasksPath = join(changeDir, "tasks.md");
1734
2080
  const designPath = join(changeDir, "design.md");
@@ -1779,7 +2125,8 @@ var OpenSpecAdapter = class {
1779
2125
  * Read raw archived change files (reactive)
1780
2126
  */
1781
2127
  async readArchivedChangeRaw(changeId) {
1782
- const archiveChangeDir = join(this.archiveDir, changeId);
2128
+ const canonicalChangeId = requireCanonicalOpenSpecEntityId(changeId, "changeId");
2129
+ const archiveChangeDir = join(this.archiveDir, canonicalChangeId);
1783
2130
  const proposalPath = join(archiveChangeDir, "proposal.md");
1784
2131
  const tasksPath = join(archiveChangeDir, "tasks.md");
1785
2132
  const designPath = join(archiveChangeDir, "design.md");
@@ -1799,26 +2146,35 @@ var OpenSpecAdapter = class {
1799
2146
  };
1800
2147
  }
1801
2148
  async writeSpec(specId, content) {
1802
- const specDir = join(this.specsDir, specId);
1803
- await mkdir(specDir, { recursive: true });
1804
- await writeFile(join(specDir, "spec.md"), content, "utf-8");
2149
+ const canonicalSpecId = requireCanonicalOpenSpecEntityId(specId, "specId");
2150
+ await writePhysicalReactiveFile({
2151
+ rootPath: this.projectDir,
2152
+ relativePath: join("openspec", "specs", canonicalSpecId, "spec.md"),
2153
+ content
2154
+ });
1805
2155
  }
1806
2156
  async writeChange(changeId, proposal, tasks) {
1807
- const changeDir = join(this.changesDir, changeId);
1808
- await mkdir(changeDir, { recursive: true });
1809
- await writeFile(join(changeDir, "proposal.md"), proposal, "utf-8");
1810
- if (tasks !== void 0) await writeFile(join(changeDir, "tasks.md"), tasks, "utf-8");
2157
+ const canonicalChangeId = requireCanonicalOpenSpecEntityId(changeId, "changeId");
2158
+ await writePhysicalReactiveFile({
2159
+ rootPath: this.projectDir,
2160
+ relativePath: join("openspec", "changes", canonicalChangeId, "proposal.md"),
2161
+ content: proposal
2162
+ });
2163
+ if (tasks !== void 0) await writePhysicalReactiveFile({
2164
+ rootPath: this.projectDir,
2165
+ relativePath: join("openspec", "changes", canonicalChangeId, "tasks.md"),
2166
+ content: tasks
2167
+ });
1811
2168
  }
1812
- async archiveChange(changeId) {
1813
- try {
1814
- const changeDir = join(this.changesDir, changeId);
1815
- const archivePath = join(this.archiveDir, changeId);
1816
- await mkdir(this.archiveDir, { recursive: true });
1817
- await rename(changeDir, archivePath);
1818
- return true;
1819
- } catch {
1820
- return false;
1821
- }
2169
+ /** Write one validated Change or Archive entity file through the shared physical/reactive owner. */
2170
+ async writeEntityFile(stage, changeId, path, content) {
2171
+ const canonicalChangeId = requireCanonicalOpenSpecEntityId(changeId, "changeId");
2172
+ const relativePath = requireOpenSpecEntityRelativePath(path, "path");
2173
+ await writePhysicalReactiveFile({
2174
+ rootPath: this.projectDir,
2175
+ relativePath: join(getOpsxEntityRootRelativePath(stage, canonicalChangeId), relativePath),
2176
+ content
2177
+ });
1822
2178
  }
1823
2179
  async init() {
1824
2180
  await mkdir(this.specsDir, { recursive: true });
@@ -1835,36 +2191,6 @@ This project uses OpenSpec for spec-driven development.
1835
2191
  - \`changes/archive/\` - Completed changes
1836
2192
  `, "utf-8");
1837
2193
  }
1838
- /**
1839
- * Toggle a task's completion status in tasks.md
1840
- * @param changeId - The change ID
1841
- * @param taskIndex - 1-based task index
1842
- * @param completed - New completion status
1843
- */
1844
- async toggleTask(changeId, taskIndex, completed) {
1845
- try {
1846
- const tasksPath = join(this.changesDir, changeId, "tasks.md");
1847
- const lines = (await readFile(tasksPath, "utf-8")).split("\n");
1848
- let currentTaskIndex = 0;
1849
- for (let i = 0; i < lines.length; i++) {
1850
- const taskMatch = lines[i].match(/^([-*]\s+)\[([ xX])\](\s+.*)$/);
1851
- if (taskMatch) {
1852
- currentTaskIndex++;
1853
- if (currentTaskIndex === taskIndex) {
1854
- const prefix = taskMatch[1];
1855
- const suffix = taskMatch[3];
1856
- lines[i] = `${prefix}${completed ? "[x]" : "[ ]"}${suffix}`;
1857
- break;
1858
- }
1859
- }
1860
- }
1861
- if (currentTaskIndex < taskIndex) return false;
1862
- await writeFile(tasksPath, lines.join("\n"), "utf-8");
1863
- return true;
1864
- } catch {
1865
- return false;
1866
- }
1867
- }
1868
2194
  async validateSpec(specId) {
1869
2195
  const spec = await this.readSpec(specId);
1870
2196
  if (!spec) return {
@@ -1898,8 +2224,8 @@ This project uses OpenSpec for spec-driven development.
1898
2224
  const validSpecs = specs.filter((s) => s !== null);
1899
2225
  const validChanges = changes.filter((c) => c !== null);
1900
2226
  const totalRequirements = validSpecs.reduce((sum, s) => sum + s.requirements.length, 0);
1901
- const totalTasks = validChanges.reduce((sum, c) => sum + c.progress.total, 0);
1902
- const completedTasks = validChanges.reduce((sum, c) => sum + c.progress.completed, 0);
2227
+ const totalTasks = validChanges.reduce((sum, change) => sum + change.trackedTaskProgress.total, 0);
2228
+ const completedTasks = validChanges.reduce((sum, change) => sum + change.trackedTaskProgress.completed, 0);
1903
2229
  return {
1904
2230
  specs: validSpecs,
1905
2231
  changes: validChanges,
@@ -1918,135 +2244,150 @@ This project uses OpenSpec for spec-driven development.
1918
2244
  };
1919
2245
 
1920
2246
  //#endregion
1921
- //#region src/schemas.ts
2247
+ //#region src/planning-config.ts
1922
2248
  /**
1923
- * Zod schemas and TypeScript types for OpenSpec documents.
2249
+ * Orthogonal intents (updated 2026-07-27 Asia/Shanghai):
2250
+ * 1. Define distinct Project Binding, Active Root, and reactive Environment Global config projections.
2251
+ * 2. Inspect launch-project Store/Reference declarations without replacing CLI Root Context truth.
2252
+ * 3. Update only binding fields while preserving unrelated YAML fields and comments.
2253
+ * 4. Describe a typed launch-write result separately from asynchronous Root Context convergence.
2254
+ * 5. Separate CLI-owned Environment Global facts from the file-native editable document owner.
1924
2255
  *
1925
- * OpenSpec uses a structured format for specifications and change proposals:
1926
- * - Spec: A specification document with requirements and scenarios
1927
- * - Change: A change proposal with deltas and tasks
1928
- * - Task: A trackable work item within a change
1929
- *
1930
- * @module schemas
1931
- */
1932
- /**
1933
- * File metadata for a change directory entry.
1934
- */
1935
- const ChangeFileSchema = z.object({
1936
- path: z.string(),
1937
- type: z.enum(["file", "directory"]),
1938
- content: z.string().optional(),
1939
- size: z.number().optional(),
1940
- mime: z.string().optional(),
1941
- previewKind: z.enum(FILE_PREVIEW_KINDS).optional()
1942
- });
1943
- const ScenarioStepKeywordSchema = z.enum([
1944
- "GIVEN",
1945
- "WHEN",
1946
- "THEN",
1947
- "AND",
1948
- "BUT"
1949
- ]);
1950
- const ScenarioStepSchema = z.object({
1951
- keyword: ScenarioStepKeywordSchema,
1952
- contentMarkdown: z.string(),
1953
- rawText: z.string()
1954
- });
1955
- /**
1956
- * A requirement within a specification.
1957
- * Requirements should use RFC 2119 keywords (SHALL, MUST, etc.)
1958
- */
1959
- const RequirementSchema = z.object({
1960
- id: z.string(),
1961
- title: z.string(),
1962
- bodyMarkdown: z.string(),
1963
- text: z.string(),
1964
- scenarios: z.array(z.object({
1965
- title: z.string(),
1966
- bodyMarkdown: z.string(),
1967
- rawText: z.string(),
1968
- steps: z.array(ScenarioStepSchema).optional()
1969
- }))
1970
- });
1971
- /**
1972
- * A specification document.
1973
- * Located at: openspec/specs/{id}/spec.md
1974
- */
1975
- const SpecSchema = z.object({
1976
- id: z.string(),
1977
- name: z.string(),
1978
- overview: z.string(),
1979
- requirements: z.array(RequirementSchema),
1980
- metadata: z.object({
1981
- version: z.string().default("1.0.0"),
1982
- format: z.literal("openspec").default("openspec"),
1983
- sourcePath: z.string().optional()
1984
- }).optional()
1985
- });
1986
- /**
1987
- * A delta describes changes to a spec within a change proposal.
1988
- * Deltas track which specs are affected and how.
1989
- */
1990
- const DeltaOperationType = z.enum([
1991
- "ADDED",
1992
- "MODIFIED",
1993
- "REMOVED",
1994
- "RENAMED"
1995
- ]);
1996
- const DeltaSchema = z.object({
1997
- spec: z.string(),
1998
- operation: DeltaOperationType,
1999
- description: z.string(),
2000
- requirement: RequirementSchema.optional(),
2001
- requirements: z.array(RequirementSchema).optional(),
2002
- rename: z.object({
2003
- from: z.string(),
2004
- to: z.string()
2005
- }).optional()
2006
- });
2007
- /**
2008
- * A task within a change proposal.
2009
- * Tasks are parsed from tasks.md using checkbox syntax: - [ ] or - [x]
2256
+ * Original request (2026-07-15): "Config ownership separates launch-project binding, active-root config, and environment-global config."
2257
+ * Original request (2026-07-18): "Profile/Drift must refresh with external environment config changes."
2258
+ * Derived requirement (2026-07-19): "Project Binding returns launch-write, root preview, and transition evidence."
2010
2259
  */
2011
- const TaskSchema = z.object({
2012
- id: z.string(),
2013
- text: z.string(),
2014
- completed: z.boolean(),
2015
- section: z.string().optional()
2016
- });
2017
- /**
2018
- * A delta spec file from changes/{id}/specs/{specId}/spec.md
2019
- * Contains the proposed changes to a spec
2020
- */
2021
- const DeltaSpecSchema = z.object({
2022
- specId: z.string(),
2023
- content: z.string()
2024
- });
2025
- /**
2026
- * A change proposal document.
2027
- * Located at: openspec/changes/{id}/proposal.md + tasks.md
2028
- *
2029
- * Change proposals describe why a change is needed, what will change,
2030
- * which specs are affected (deltas), and trackable tasks.
2031
- */
2032
- const ChangeSchema = z.object({
2033
- id: z.string(),
2034
- name: z.string(),
2035
- why: z.string(),
2036
- whatChanges: z.string(),
2037
- deltas: z.array(DeltaSchema),
2038
- tasks: z.array(TaskSchema),
2039
- progress: z.object({
2040
- total: z.number(),
2041
- completed: z.number()
2042
- }),
2043
- design: z.string().optional(),
2044
- deltaSpecs: z.array(DeltaSpecSchema).optional(),
2045
- metadata: z.object({
2046
- version: z.string().default("1.0.0"),
2047
- format: z.literal("openspec-change").default("openspec-change")
2048
- }).optional()
2049
- });
2260
+ /** Runtime schema for JSON values accepted by OpenSpec global configuration. */
2261
+ const PlanningConfigJsonValueSchema = z.lazy(() => z.union([
2262
+ z.string(),
2263
+ z.number(),
2264
+ z.boolean(),
2265
+ z.null(),
2266
+ z.array(PlanningConfigJsonValueSchema),
2267
+ z.record(z.string(), PlanningConfigJsonValueSchema)
2268
+ ]));
2269
+ /** Runtime schema for the complete environment-global OpenSpec configuration document. */
2270
+ const EnvironmentGlobalConfigValueSchema = z.record(z.string(), PlanningConfigJsonValueSchema);
2271
+ /** Runtime schema for one project-declared Store Reference entry. */
2272
+ const PlanningConfigReferenceSchema = z.object({
2273
+ id: z.string().min(1),
2274
+ remote: z.string().min(1).optional()
2275
+ }).strict();
2276
+ /** Runtime schema for project binding mutations. */
2277
+ const ProjectBindingUpdateSchema = z.object({
2278
+ store: z.string().min(1).nullable().optional(),
2279
+ references: z.array(PlanningConfigReferenceSchema).nullable().optional()
2280
+ }).refine((value) => value.store !== void 0 || value.references !== void 0, { message: "At least one Project Binding field is required." });
2281
+ function isRecord(value) {
2282
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2283
+ }
2284
+ function inspectReference(value) {
2285
+ if (typeof value === "string" && value.length > 0) return { id: value };
2286
+ if (!isRecord(value) || typeof value.id !== "string" || value.id.length === 0) return null;
2287
+ if (value.remote === void 0) return { id: value.id };
2288
+ if (typeof value.remote !== "string" || value.remote.length === 0) return null;
2289
+ return {
2290
+ id: value.id,
2291
+ remote: value.remote
2292
+ };
2293
+ }
2294
+ /** Inspect only launch-project binding declarations; Root Context remains the effective projection. */
2295
+ function inspectProjectBinding(content) {
2296
+ const absent = {
2297
+ store: {
2298
+ state: "absent",
2299
+ id: null
2300
+ },
2301
+ references: {
2302
+ state: "absent",
2303
+ entries: []
2304
+ },
2305
+ diagnostics: []
2306
+ };
2307
+ if (content === null || content.trim().length === 0) return absent;
2308
+ const document = parseDocument(content);
2309
+ if (document.errors.length > 0) return {
2310
+ ...absent,
2311
+ diagnostics: [{
2312
+ code: "config-unparseable",
2313
+ message: document.errors.map((error) => error.message).join("\n")
2314
+ }]
2315
+ };
2316
+ const raw = document.toJS();
2317
+ if (raw === null) return absent;
2318
+ if (!isRecord(raw)) return {
2319
+ ...absent,
2320
+ diagnostics: [{
2321
+ code: "config-not-mapping",
2322
+ message: "OpenSpec config must be a YAML mapping."
2323
+ }]
2324
+ };
2325
+ const diagnostics = [];
2326
+ let store = {
2327
+ state: "absent",
2328
+ id: null
2329
+ };
2330
+ if (raw.store !== void 0) if (typeof raw.store === "string" && raw.store.length > 0) store = {
2331
+ state: "declared",
2332
+ id: raw.store
2333
+ };
2334
+ else {
2335
+ store = {
2336
+ state: "invalid",
2337
+ id: null
2338
+ };
2339
+ diagnostics.push({
2340
+ code: "store-not-string",
2341
+ message: "The store field must be one non-empty Store id string."
2342
+ });
2343
+ }
2344
+ let references = {
2345
+ state: "absent",
2346
+ entries: []
2347
+ };
2348
+ if (raw.references !== void 0) if (!Array.isArray(raw.references)) {
2349
+ references = {
2350
+ state: "invalid",
2351
+ entries: []
2352
+ };
2353
+ diagnostics.push({
2354
+ code: "references-not-array",
2355
+ message: "The references field must be an array of Store declarations."
2356
+ });
2357
+ } else {
2358
+ const entries = [];
2359
+ let invalid = false;
2360
+ for (const value of raw.references) {
2361
+ const reference = inspectReference(value);
2362
+ if (reference) entries.push(reference);
2363
+ else invalid = true;
2364
+ }
2365
+ references = {
2366
+ state: invalid ? "invalid" : "declared",
2367
+ entries
2368
+ };
2369
+ if (invalid) diagnostics.push({
2370
+ code: "reference-entry-invalid",
2371
+ message: "One or more Reference declarations are not a Store id or { id, remote }."
2372
+ });
2373
+ }
2374
+ return {
2375
+ store,
2376
+ references,
2377
+ diagnostics
2378
+ };
2379
+ }
2380
+ /** Update only `store` and `references`, retaining all unrelated YAML nodes and comments. */
2381
+ function updateProjectBindingContent(content, update) {
2382
+ const document = parseDocument(content?.trim() ? content : "{}\n");
2383
+ if (document.errors.length > 0) throw new Error(document.errors.map((error) => error.message).join("\n"));
2384
+ if (document.contents !== null && !isMap(document.contents)) throw new Error("OpenSpec config must be a YAML mapping before Project Binding can be edited.");
2385
+ if (update.store !== void 0) if (update.store === null) document.delete("store");
2386
+ else document.set("store", update.store);
2387
+ if (update.references !== void 0) if (update.references === null) document.delete("references");
2388
+ else document.set("references", update.references.map((reference) => reference.remote === void 0 ? reference.id : reference));
2389
+ return document.toString({ lineWidth: 0 });
2390
+ }
2050
2391
 
2051
2392
  //#endregion
2052
2393
  //#region src/watcher.ts
@@ -2327,6 +2668,20 @@ function runBufferedCommand(options) {
2327
2668
 
2328
2669
  //#endregion
2329
2670
  //#region src/config.ts
2671
+ /**
2672
+ * Orthogonal intents (updated 2026-07-20 Asia/Shanghai):
2673
+ * 1. Define and persist project-scoped OpenSpecUI settings.
2674
+ * 2. Resolve, diagnose, cache, and explicitly invalidate the OpenSpec CLI runner.
2675
+ * 3. Prevent retired in-flight runner resolutions from reclaiming cache ownership.
2676
+ * 4. Preserve reactive config reads and serialized config writes.
2677
+ *
2678
+ * Original request (2026-07-14): "openspec 1.6.0 已经放出,我们需要开始进行适配。"
2679
+ * Independent review correction (2026-07-20): Global CLI installation must retire cached and
2680
+ * in-flight runner authority.
2681
+ *
2682
+ * Compromise: configuration schemas and runtime management remain in one historical physical file;
2683
+ * splitting that public surface is outside the bounded 6.13 correction.
2684
+ */
2330
2685
  const execAsync = promisify(exec);
2331
2686
  const execFileAsync = promisify(execFile);
2332
2687
  const CLI_PROBE_TIMEOUT_MS = 2e4;
@@ -3078,6 +3433,7 @@ var ConfigManager = class {
3078
3433
  configPath;
3079
3434
  projectDir;
3080
3435
  resolvedRunner = null;
3436
+ resolvedRunnerGeneration = 0;
3081
3437
  resolvingRunnerPromise = null;
3082
3438
  writeChain = Promise.resolve();
3083
3439
  constructor(projectDir) {
@@ -3205,13 +3561,15 @@ var ConfigManager = class {
3205
3561
  async resolveCliRunner() {
3206
3562
  if (this.resolvedRunner) return this.resolvedRunner;
3207
3563
  if (this.resolvingRunnerPromise) return this.resolvingRunnerPromise;
3208
- this.resolvingRunnerPromise = this.resolveCliRunnerUncached().then((runner) => {
3209
- this.resolvedRunner = runner;
3564
+ const generation = this.resolvedRunnerGeneration;
3565
+ const resolution = this.resolveCliRunnerUncached().then((runner) => {
3566
+ if (this.resolvedRunnerGeneration === generation && this.resolvingRunnerPromise === resolution) this.resolvedRunner = runner;
3210
3567
  return runner;
3211
3568
  }).finally(() => {
3212
- this.resolvingRunnerPromise = null;
3569
+ if (this.resolvingRunnerPromise === resolution) this.resolvingRunnerPromise = null;
3213
3570
  });
3214
- return this.resolvingRunnerPromise;
3571
+ this.resolvingRunnerPromise = resolution;
3572
+ return resolution;
3215
3573
  }
3216
3574
  async resolveCliRunnerUncached() {
3217
3575
  const config = await this.readConfig();
@@ -3244,9 +3602,13 @@ var ConfigManager = class {
3244
3602
  return this.resolveCliRunner();
3245
3603
  }
3246
3604
  /**
3247
- * 清理 CLI 解析缓存(用于 ENOENT 自愈)
3605
+ * Retire the resolved and in-flight CLI runner generation.
3606
+ *
3607
+ * Existing callers still receive their own resolution result, but a retired resolution cannot
3608
+ * repopulate the shared cache or clear a newer pending owner.
3248
3609
  */
3249
3610
  invalidateResolvedCliRunner() {
3611
+ this.resolvedRunnerGeneration += 1;
3250
3612
  this.resolvedRunner = null;
3251
3613
  this.resolvingRunnerPromise = null;
3252
3614
  }
@@ -3729,8 +4091,61 @@ function buildRuntimePackageInstallCommand(input) {
3729
4091
  }) ?? null;
3730
4092
  }
3731
4093
 
4094
+ //#endregion
4095
+ //#region src/cli-stream-child-owner.ts
4096
+ /** Internal owner for the direct child of one CLI stream. */
4097
+ var CliStreamChildOwner = class {
4098
+ #currentChild = null;
4099
+ #releaseCount = 0;
4100
+ claim(child) {
4101
+ if (this.#currentChild !== null) throw new Error("Cannot claim a second child before releasing the current CLI stream child");
4102
+ this.#currentChild = child;
4103
+ ownersByChild.set(child, this);
4104
+ }
4105
+ owns(child) {
4106
+ return this.#currentChild === child;
4107
+ }
4108
+ release(child) {
4109
+ if (!this.owns(child)) return false;
4110
+ this.#currentChild = null;
4111
+ this.#releaseCount += 1;
4112
+ return true;
4113
+ }
4114
+ get currentChild() {
4115
+ return this.#currentChild;
4116
+ }
4117
+ inspect() {
4118
+ return {
4119
+ currentChild: this.#currentChild,
4120
+ releaseCount: this.#releaseCount
4121
+ };
4122
+ }
4123
+ };
4124
+ const ownersByChild = /* @__PURE__ */ new WeakMap();
4125
+
3732
4126
  //#endregion
3733
4127
  //#region src/cli-executor.ts
4128
+ /** Failure raised when even forced termination cannot confirm child-process close. */
4129
+ var CliStreamTerminationError = class extends Error {
4130
+ constructor(command) {
4131
+ super(`CLI stream did not close after forced termination: ${command}`);
4132
+ this.name = "CliStreamTerminationError";
4133
+ }
4134
+ };
4135
+ const STREAM_TERMINATION_GRACE_MS = 1e3;
4136
+ const STREAM_FORCE_CLOSE_TIMEOUT_MS = 1e3;
4137
+ function createDeferred() {
4138
+ let resolve$1;
4139
+ let reject;
4140
+ return {
4141
+ promise: new Promise((resolvePromise, rejectPromise) => {
4142
+ resolve$1 = resolvePromise;
4143
+ reject = rejectPromise;
4144
+ }),
4145
+ resolve: resolve$1,
4146
+ reject
4147
+ };
4148
+ }
3734
4149
  /**
3735
4150
  * CLI 执行器
3736
4151
  *
@@ -3738,9 +4153,11 @@ function buildRuntimePackageInstallCommand(input) {
3738
4153
  * 所有命令都使用 shell: false,避免 shell 注入风险。
3739
4154
  */
3740
4155
  var CliExecutor = class {
4156
+ contracts;
3741
4157
  constructor(configManager, projectDir) {
3742
4158
  this.configManager = configManager;
3743
4159
  this.projectDir = projectDir;
4160
+ this.contracts = new OpenSpecCliContractExecutor((args) => this.execute(args));
3744
4161
  }
3745
4162
  async buildCommandArray(args) {
3746
4163
  return [...await this.configManager.getCliCommand(), ...args];
@@ -3828,8 +4245,8 @@ var CliExecutor = class {
3828
4245
  */
3829
4246
  async validate(type, id) {
3830
4247
  const args = ["validate"];
3831
- if (type) args.push(type);
3832
4248
  if (id) args.push(id);
4249
+ if (type) args.push("--type", type);
3833
4250
  return this.execute(args);
3834
4251
  }
3835
4252
  /**
@@ -3858,37 +4275,14 @@ var CliExecutor = class {
3858
4275
  return this.execute(args);
3859
4276
  }
3860
4277
  /**
3861
- * 执行 openspec store list --json
3862
- *
3863
- * Beta 功能(Stores)。返回原始 CliResult;数据归类(ok / 异常一数据不兼容 /
3864
- * 异常二指令变更)由调用方用 classifyStoreCliOutput 完成。这样 CliExecutor 保持单一职责。
3865
- * Spec: openspec-cli-integration › Stores CLI Query Mapping / Beta Feature Fault Tolerance。
3866
- */
3867
- async listStores() {
3868
- return this.execute([
3869
- "store",
3870
- "list",
3871
- "--json"
3872
- ]);
3873
- }
3874
- /**
3875
- * 执行 openspec store doctor [--json] [id]
3876
- *
3877
- * Beta 功能(Stores)。同 listStores,归类交给调用方。
3878
- */
3879
- async doctorStores(id) {
3880
- const args = ["store", "doctor"];
3881
- if (id) args.push(id);
3882
- args.push("--json");
3883
- return this.execute(args);
3884
- }
3885
- /**
3886
4278
  * 流式执行 openspec validate
3887
4279
  */
3888
- validateStream(type, id, onEvent) {
4280
+ validateStream(options, onEvent) {
3889
4281
  const args = ["validate"];
3890
- if (type) args.push(type);
3891
- if (id) args.push(id);
4282
+ if (options.id) args.push(options.id);
4283
+ if (options.type) args.push("--type", options.type);
4284
+ if (options.strict) args.push("--strict");
4285
+ if (options.store !== void 0) args.push("--store", options.store);
3892
4286
  return this.executeStream(args, onEvent);
3893
4287
  }
3894
4288
  /**
@@ -3920,28 +4314,78 @@ var CliExecutor = class {
3920
4314
  /**
3921
4315
  * 流式执行 CLI 命令
3922
4316
  */
3923
- async executeStream(args, onEvent) {
3924
- let cancelled = false;
3925
- let activeChild = null;
4317
+ executeStream(args, onEvent) {
4318
+ return this.createStreamHandle(async () => this.buildCommandArray(args), onEvent, () => this.configManager.invalidateResolvedCliRunner());
4319
+ }
4320
+ createStreamHandle(resolveCommand, onEvent, retryRunner) {
4321
+ const settlement = createDeferred();
4322
+ let settled = false;
4323
+ let cancelRequested = false;
4324
+ let terminationStarted = false;
4325
+ const childOwner = new CliStreamChildOwner();
4326
+ let activeCommand = "CLI command";
4327
+ let terminationTimer = null;
4328
+ let forceCloseTimer = null;
4329
+ const clearTerminationTimers = () => {
4330
+ if (terminationTimer) clearTimeout(terminationTimer);
4331
+ if (forceCloseTimer) clearTimeout(forceCloseTimer);
4332
+ terminationTimer = null;
4333
+ forceCloseTimer = null;
4334
+ };
4335
+ const settle = (value) => {
4336
+ if (settled) return;
4337
+ settled = true;
4338
+ clearTerminationTimers();
4339
+ onEvent({
4340
+ type: "exit",
4341
+ exitCode: value.exitCode
4342
+ });
4343
+ settlement.resolve(value);
4344
+ };
4345
+ const failTermination = () => {
4346
+ if (settled) return;
4347
+ settled = true;
4348
+ clearTerminationTimers();
4349
+ settlement.reject(new CliStreamTerminationError(activeCommand));
4350
+ };
4351
+ const requestChildTermination = (child) => {
4352
+ if (terminationStarted || settled) return;
4353
+ terminationStarted = true;
4354
+ try {
4355
+ child.kill("SIGTERM");
4356
+ } catch {}
4357
+ terminationTimer = setTimeout(() => {
4358
+ if (settled || !childOwner.owns(child)) return;
4359
+ try {
4360
+ child.kill("SIGKILL");
4361
+ } catch {}
4362
+ forceCloseTimer = setTimeout(() => {
4363
+ if (!settled && childOwner.owns(child)) failTermination();
4364
+ }, STREAM_FORCE_CLOSE_TIMEOUT_MS);
4365
+ }, STREAM_TERMINATION_GRACE_MS);
4366
+ };
3926
4367
  const start = async (allowRetry) => {
3927
- if (cancelled) return;
4368
+ if (cancelRequested || settled) return;
3928
4369
  let fullCommand;
3929
4370
  try {
3930
- fullCommand = await this.buildCommandArray(args);
4371
+ fullCommand = await resolveCommand();
3931
4372
  } catch (err) {
4373
+ if (cancelRequested || settled) return;
3932
4374
  onEvent({
3933
4375
  type: "stderr",
3934
4376
  data: err instanceof Error ? err.message : String(err)
3935
4377
  });
3936
- onEvent({
3937
- type: "exit",
4378
+ settle({
4379
+ reason: "startup-failed",
3938
4380
  exitCode: null
3939
4381
  });
3940
4382
  return;
3941
4383
  }
4384
+ if (cancelRequested || settled) return;
4385
+ activeCommand = fullCommand.join(" ");
3942
4386
  onEvent({
3943
4387
  type: "command",
3944
- data: fullCommand.join(" ")
4388
+ data: activeCommand
3945
4389
  });
3946
4390
  const [cmd, ...cmdArgs] = fullCommand;
3947
4391
  const started = spawnSafe(cmd, cmdArgs, {
@@ -3951,8 +4395,8 @@ var CliExecutor = class {
3951
4395
  });
3952
4396
  if (!started.ok) {
3953
4397
  const { code, message } = started.error;
3954
- if (allowRetry && code === "ENOENT" && !cancelled) {
3955
- this.configManager.invalidateResolvedCliRunner();
4398
+ if (allowRetry && code === "ENOENT" && !cancelRequested && retryRunner) {
4399
+ retryRunner();
3956
4400
  start(false);
3957
4401
  return;
3958
4402
  }
@@ -3960,14 +4404,18 @@ var CliExecutor = class {
3960
4404
  type: "stderr",
3961
4405
  data: message
3962
4406
  });
3963
- onEvent({
3964
- type: "exit",
4407
+ settle({
4408
+ reason: "startup-failed",
3965
4409
  exitCode: null
3966
4410
  });
3967
4411
  return;
3968
4412
  }
3969
4413
  const child = started.child;
3970
- activeChild = child;
4414
+ childOwner.claim(child);
4415
+ let childSpawned = false;
4416
+ child.once("spawn", () => {
4417
+ childSpawned = true;
4418
+ });
3971
4419
  child.stdout?.on("data", (data) => {
3972
4420
  onEvent({
3973
4421
  type: "stdout",
@@ -3981,19 +4429,26 @@ var CliExecutor = class {
3981
4429
  });
3982
4430
  });
3983
4431
  child.on("close", (exitCode) => {
3984
- if (activeChild !== child) return;
3985
- activeChild = null;
3986
- onEvent({
3987
- type: "exit",
4432
+ if (!childOwner.owns(child)) return;
4433
+ childOwner.release(child);
4434
+ settle({
4435
+ reason: cancelRequested ? "cancelled" : "exited",
3988
4436
  exitCode
3989
4437
  });
3990
4438
  });
3991
4439
  child.on("error", (err) => {
3992
- if (activeChild !== child) return;
3993
- activeChild = null;
4440
+ if (!childOwner.owns(child)) return;
3994
4441
  const { code, message } = formatSpawnError(err);
3995
- if (allowRetry && code === "ENOENT" && !cancelled) {
3996
- this.configManager.invalidateResolvedCliRunner();
4442
+ if (childSpawned) {
4443
+ onEvent({
4444
+ type: "stderr",
4445
+ data: message
4446
+ });
4447
+ return;
4448
+ }
4449
+ childOwner.release(child);
4450
+ if (allowRetry && code === "ENOENT" && !cancelRequested && retryRunner) {
4451
+ retryRunner();
3997
4452
  start(false);
3998
4453
  return;
3999
4454
  }
@@ -4001,18 +4456,29 @@ var CliExecutor = class {
4001
4456
  type: "stderr",
4002
4457
  data: message
4003
4458
  });
4004
- onEvent({
4005
- type: "exit",
4459
+ settle({
4460
+ reason: cancelRequested ? "cancelled" : "startup-failed",
4006
4461
  exitCode: null
4007
4462
  });
4008
4463
  });
4009
4464
  };
4010
- await start(true);
4011
- return () => {
4012
- cancelled = true;
4013
- activeChild?.kill();
4014
- activeChild = null;
4465
+ start(true);
4466
+ const handle = {
4467
+ settled: settlement.promise,
4468
+ cancel: () => {
4469
+ if (cancelRequested || settled) return settlement.promise;
4470
+ cancelRequested = true;
4471
+ const child = childOwner.currentChild;
4472
+ if (child) requestChildTermination(child);
4473
+ else settle({
4474
+ reason: "cancelled",
4475
+ exitCode: null
4476
+ });
4477
+ return settlement.promise;
4478
+ }
4015
4479
  };
4480
+ settlement.promise.catch(() => {});
4481
+ return handle;
4016
4482
  }
4017
4483
  /**
4018
4484
  * 流式执行 openspec init
@@ -4038,6 +4504,7 @@ var CliExecutor = class {
4038
4504
  ];
4039
4505
  if (options.skipSpecs) args.push("--skip-specs");
4040
4506
  if (options.noValidate) args.push("--no-validate");
4507
+ if (options.store !== void 0) args.push("--store", options.store);
4041
4508
  return this.executeStream(args, onEvent);
4042
4509
  }
4043
4510
  /**
@@ -4048,89 +4515,421 @@ var CliExecutor = class {
4048
4515
  */
4049
4516
  executeCommandStream(command, onEvent) {
4050
4517
  const [cmd, ...cmdArgs] = command;
4051
- if (cmd === "openspec") {
4052
- let cancelResolved = null;
4053
- let cancelled = false;
4054
- this.executeStream([...cmdArgs], onEvent).then((cancel) => {
4055
- if (cancelled) {
4056
- cancel();
4057
- return;
4058
- }
4059
- cancelResolved = cancel;
4060
- }).catch((err) => {
4061
- onEvent({
4062
- type: "stderr",
4063
- data: err instanceof Error ? err.message : String(err)
4064
- });
4065
- onEvent({
4066
- type: "exit",
4067
- exitCode: null
4068
- });
4069
- });
4070
- return () => {
4071
- cancelled = true;
4072
- cancelResolved?.();
4073
- };
4518
+ if (cmd === "openspec") return this.executeStream([...cmdArgs], onEvent);
4519
+ return this.createStreamHandle(() => [cmd, ...cmdArgs], onEvent);
4520
+ }
4521
+ };
4522
+
4523
+ //#endregion
4524
+ //#region src/runtime-invalidation.ts
4525
+ /**
4526
+ * Orthogonal intents (created 2026-07-16 Asia/Shanghai):
4527
+ * 1. Define runtime invalidation facet identity without carrying projected domain data.
4528
+ * 2. Expose monotonic generations as ReactiveContext dependencies.
4529
+ * 3. Coalesce direct-listener pushes per facet while generations advance synchronously.
4530
+ *
4531
+ * Original request (2026-07-15): "Push 通知变更,然后让多端基于订阅拉取更新。"
4532
+ */
4533
+ /** Complete runtime projection facet vocabulary. */
4534
+ const RUNTIME_INVALIDATION_FACETS = [
4535
+ "project",
4536
+ "stores",
4537
+ "worksets",
4538
+ "schemas",
4539
+ "context"
4540
+ ];
4541
+ /** Environment-local invalidation identity. Authoritative data stays in CLI-backed projections. */
4542
+ var RuntimeInvalidationIndex = class {
4543
+ generations = /* @__PURE__ */ new Map();
4544
+ states = /* @__PURE__ */ new Map();
4545
+ listeners = /* @__PURE__ */ new Set();
4546
+ pendingNotifications = /* @__PURE__ */ new Map();
4547
+ notificationScheduled = false;
4548
+ constructor() {
4549
+ for (const facet of RUNTIME_INVALIDATION_FACETS) {
4550
+ this.generations.set(facet, 0);
4551
+ this.states.set(facet, new ReactiveState(0));
4074
4552
  }
4075
- onEvent({
4076
- type: "command",
4077
- data: command.join(" ")
4078
- });
4079
- const started = spawnSafe(cmd, cmdArgs, {
4080
- cwd: this.projectDir,
4081
- shell: false,
4082
- env: createCleanCliEnv()
4553
+ }
4554
+ current(facet) {
4555
+ return this.generations.get(facet) ?? 0;
4556
+ }
4557
+ track(...facets) {
4558
+ return facets.map((facet) => ({
4559
+ facet,
4560
+ generation: this.states.get(facet)?.get() ?? 0
4561
+ }));
4562
+ }
4563
+ invalidate(facets) {
4564
+ const tokens = [...new Set(facets)].map((facet) => {
4565
+ const generation = this.current(facet) + 1;
4566
+ this.generations.set(facet, generation);
4567
+ this.states.get(facet)?.set(generation);
4568
+ return {
4569
+ facet,
4570
+ generation
4571
+ };
4083
4572
  });
4084
- if (!started.ok) {
4085
- onEvent({
4086
- type: "stderr",
4087
- data: started.error.message
4088
- });
4089
- onEvent({
4090
- type: "exit",
4091
- exitCode: null
4092
- });
4093
- return () => {};
4573
+ this.enqueueNotifications(tokens);
4574
+ return tokens;
4575
+ }
4576
+ enqueueNotifications(tokens) {
4577
+ for (const record of this.listeners) {
4578
+ let pending = this.pendingNotifications.get(record);
4579
+ for (const token of tokens) {
4580
+ if (!record.facets.has(token.facet)) continue;
4581
+ pending ??= /* @__PURE__ */ new Map();
4582
+ pending.set(token.facet, token);
4583
+ }
4584
+ if (pending) this.pendingNotifications.set(record, pending);
4094
4585
  }
4095
- const child = started.child;
4096
- child.stdout?.on("data", (data) => {
4097
- onEvent({
4098
- type: "stdout",
4099
- data: data.toString()
4100
- });
4101
- });
4102
- child.stderr?.on("data", (data) => {
4103
- onEvent({
4104
- type: "stderr",
4105
- data: data.toString()
4586
+ if (this.pendingNotifications.size === 0 || this.notificationScheduled) return;
4587
+ this.notificationScheduled = true;
4588
+ queueMicrotask(() => this.flushNotifications());
4589
+ }
4590
+ flushNotifications() {
4591
+ const notifications = this.pendingNotifications;
4592
+ this.pendingNotifications = /* @__PURE__ */ new Map();
4593
+ this.notificationScheduled = false;
4594
+ for (const [record, pending] of notifications) {
4595
+ if (!this.listeners.has(record)) continue;
4596
+ const tokens = RUNTIME_INVALIDATION_FACETS.flatMap((facet) => {
4597
+ const token = pending.get(facet);
4598
+ return token ? [token] : [];
4106
4599
  });
4107
- });
4108
- child.on("close", (exitCode) => {
4109
- onEvent({
4110
- type: "exit",
4111
- exitCode
4600
+ if (tokens.length === 0) continue;
4601
+ try {
4602
+ record.listener(tokens);
4603
+ } catch (error) {
4604
+ console.error("Runtime invalidation listener failed:", error);
4605
+ }
4606
+ }
4607
+ }
4608
+ subscribe(facets, listener) {
4609
+ const record = {
4610
+ facets: new Set(facets),
4611
+ listener
4612
+ };
4613
+ this.listeners.add(record);
4614
+ return () => {
4615
+ this.listeners.delete(record);
4616
+ this.pendingNotifications.delete(record);
4617
+ };
4618
+ }
4619
+ };
4620
+
4621
+ //#endregion
4622
+ //#region src/open-spec-data-home-observer.ts
4623
+ /**
4624
+ * Orthogonal intents (updated 2026-07-28 Asia/Shanghai):
4625
+ * 1. Own the effective OpenSpec data-home observation lease for one runtime environment.
4626
+ * 2. Map registry, Workset, and schema paths to their objective invalidation facets.
4627
+ * 3. Keep Store, Workset, schema, and Context projections responsible for fresh pulls.
4628
+ * 4. Release the path subscription and root lease deterministically.
4629
+ * 5. Settle initially missing official targets from bounded ancestor creation without invalidating unrelated content.
4630
+ *
4631
+ * Original request (2026-07-15): "有效 OpenSpec data home 的变化要让所有端拉取最新投影。"
4632
+ * Original request (2026-07-26): "真正基于文件、甚至是文件内容结构的变更去拉取更新。"
4633
+ * Remote CI fixed point (2026-07-28): data-home Schema creation may arrive as an ancestor event on Linux.
4634
+ */
4635
+ /** Official OpenSpec 1.6 data-home paths and the projections whose inputs they can change. */
4636
+ const OPEN_SPEC_DATA_HOME_OBSERVATION_TARGETS = [
4637
+ {
4638
+ relativePath: "stores/registry.yaml",
4639
+ recursive: false,
4640
+ facets: ["stores", "context"]
4641
+ },
4642
+ {
4643
+ relativePath: "worksets",
4644
+ recursive: true,
4645
+ facets: ["worksets"]
4646
+ },
4647
+ {
4648
+ relativePath: "schemas",
4649
+ recursive: true,
4650
+ facets: ["schemas", "context"]
4651
+ }
4652
+ ];
4653
+ /** Observe one effective OpenSpec data home without parsing or owning its domain data. */
4654
+ var OpenSpecDataHomeObserver = class {
4655
+ dataHomePath;
4656
+ environment;
4657
+ invalidation;
4658
+ startPromise = null;
4659
+ releasePathSubscriptions = [];
4660
+ releaseRoot = null;
4661
+ disposed = false;
4662
+ state = "idle";
4663
+ constructor(options) {
4664
+ this.dataHomePath = options.dataHomePath;
4665
+ this.environment = options.environment;
4666
+ this.invalidation = options.invalidation;
4667
+ }
4668
+ start() {
4669
+ if (this.disposed) return Promise.reject(/* @__PURE__ */ new Error("Cannot start data-home observation after teardown."));
4670
+ if (this.startPromise) return this.startPromise;
4671
+ this.state = "starting";
4672
+ this.releasePathSubscriptions = OPEN_SPEC_DATA_HOME_OBSERVATION_TARGETS.map((target) => {
4673
+ const targetPath = join$1(this.dataHomePath, target.relativePath);
4674
+ let wasPresent = existsSync(targetPath);
4675
+ return acquireWatcher(targetPath, () => {
4676
+ const isPresent = existsSync(targetPath);
4677
+ if (wasPresent || isPresent) this.invalidation.invalidate(target.facets);
4678
+ wasPresent = isPresent;
4679
+ }, {
4680
+ recursive: target.recursive,
4681
+ watchAncestorsWhileMissing: true
4112
4682
  });
4113
4683
  });
4114
- child.on("error", (err) => {
4115
- const { message } = formatSpawnError(err);
4116
- onEvent({
4117
- type: "stderr",
4118
- data: message
4119
- });
4120
- onEvent({
4121
- type: "exit",
4122
- exitCode: null
4123
- });
4684
+ const startPromise = this.environment.acquireRoot(this.dataHomePath).then(async (releaseRoot) => {
4685
+ if (this.disposed) {
4686
+ await releaseRoot();
4687
+ return;
4688
+ }
4689
+ this.releaseRoot = releaseRoot;
4690
+ this.state = "active";
4691
+ }).catch((error) => {
4692
+ this.releasePathSubscriptions.forEach((release) => release());
4693
+ this.releasePathSubscriptions = [];
4694
+ if (!this.disposed) this.state = "failed";
4695
+ if (this.startPromise === startPromise) this.startPromise = null;
4696
+ throw error;
4124
4697
  });
4698
+ this.startPromise = startPromise;
4699
+ return this.startPromise;
4700
+ }
4701
+ /** Current lease state used by the bounded watcher-failure fallback. */
4702
+ getState() {
4703
+ return this.state;
4704
+ }
4705
+ async dispose() {
4706
+ if (this.disposed) return;
4707
+ this.disposed = true;
4708
+ this.state = "disposed";
4709
+ this.releasePathSubscriptions.forEach((release) => release());
4710
+ this.releasePathSubscriptions = [];
4711
+ await this.startPromise?.catch(() => {});
4712
+ const releaseRoot = this.releaseRoot;
4713
+ this.releaseRoot = null;
4714
+ await releaseRoot?.();
4715
+ }
4716
+ };
4717
+
4718
+ //#endregion
4719
+ //#region src/open-spec-data-scope.ts
4720
+ /**
4721
+ * Orthogonal intents (created 2026-07-16 Asia/Shanghai):
4722
+ * 1. Project the inherited OpenSpec user-data root without introducing project overrides.
4723
+ * 2. Match OpenSpec 1.6 XDG and platform fallback rules exactly.
4724
+ * 3. Preserve the environment source as read-only diagnostic evidence.
4725
+ *
4726
+ * Original request (2026-07-15): "OpenSpecUI inherits the launch environment's XDG_DATA_HOME."
4727
+ */
4728
+ /** Directory name appended to the selected platform user-data root. */
4729
+ const OPEN_SPEC_DATA_DIRECTORY_NAME = "openspec";
4730
+ function joinPlatformPath(platform$1, ...segments) {
4731
+ return platform$1 === "win32" ? win32.join(...segments) : posix.join(...segments);
4732
+ }
4733
+ /** Resolve the read-only data-scope diagnostic using OpenSpec 1.6's platform rules. */
4734
+ function resolveOpenSpecDataScope(options = {}) {
4735
+ const env = options.env ?? process.env;
4736
+ const platform$1 = options.platform ?? platform();
4737
+ const homedir$1 = options.homedir ?? homedir();
4738
+ if (env.XDG_DATA_HOME) return {
4739
+ path: joinPlatformPath(platform$1, env.XDG_DATA_HOME, OPEN_SPEC_DATA_DIRECTORY_NAME),
4740
+ source: "xdg-data-home",
4741
+ environmentVariable: "XDG_DATA_HOME"
4742
+ };
4743
+ if (platform$1 === "win32" && env.LOCALAPPDATA) return {
4744
+ path: joinPlatformPath(platform$1, env.LOCALAPPDATA, OPEN_SPEC_DATA_DIRECTORY_NAME),
4745
+ source: "local-app-data",
4746
+ environmentVariable: "LOCALAPPDATA"
4747
+ };
4748
+ return {
4749
+ path: platform$1 === "win32" ? joinPlatformPath(platform$1, homedir$1, "AppData", "Local", OPEN_SPEC_DATA_DIRECTORY_NAME) : joinPlatformPath(platform$1, homedir$1, ".local", "share", OPEN_SPEC_DATA_DIRECTORY_NAME),
4750
+ source: "user-home-default",
4751
+ environmentVariable: null
4752
+ };
4753
+ }
4754
+
4755
+ //#endregion
4756
+ //#region src/root-context.ts
4757
+ /** Derive the only CLI selector that preserves an already resolved Root Context. */
4758
+ function getRootContextCliSelector(rootContext) {
4759
+ const planningRoot = rootContext.planningRoot;
4760
+ if (!planningRoot) throw new Error("Cannot derive a CLI selector without a resolved planning root.");
4761
+ if (planningRoot.source !== "store") return {};
4762
+ if (!rootContext.storeId) throw new Error("OpenSpec reported an explicit Store root without a Store id.");
4763
+ return { store: rootContext.storeId };
4764
+ }
4765
+ function commandMessage(evidence, fallback) {
4766
+ return evidence.contractError || evidence.stderr.trim() || evidence.diagnostics.map((diagnostic) => diagnostic.message).join("\n") || fallback;
4767
+ }
4768
+ function publicCommandEvidence(evidence) {
4769
+ if (!evidence) return null;
4770
+ return {
4771
+ success: evidence.success,
4772
+ stdout: evidence.stdout,
4773
+ stderr: evidence.stderr,
4774
+ exitCode: evidence.exitCode,
4775
+ diagnostics: evidence.diagnostics,
4776
+ ...evidence.contractError ? { contractError: evidence.contractError } : {}
4777
+ };
4778
+ }
4779
+ function createRootContext(options, cli, doctor, context, observedAt) {
4780
+ const doctorData = doctor?.data ?? null;
4781
+ const contextData = context?.data ?? null;
4782
+ const planningRoot = doctorData?.root ?? null;
4783
+ return {
4784
+ launchProject: { path: options.launchProjectDir },
4785
+ planningRoot,
4786
+ storeId: planningRoot?.store_id ?? doctorData?.store?.id ?? null,
4787
+ cli,
4788
+ references: doctorData?.references ?? [],
4789
+ contextMembers: contextData?.members ?? [],
4790
+ dataScope: resolveOpenSpecDataScope(options),
4791
+ diagnostics: {
4792
+ root: planningRoot?.status ?? [],
4793
+ doctor: doctorData?.status ?? doctor?.diagnostics ?? [],
4794
+ context: contextData?.status ?? context?.diagnostics ?? []
4795
+ },
4796
+ evidence: {
4797
+ doctor: publicCommandEvidence(doctor),
4798
+ context: publicCommandEvidence(context)
4799
+ },
4800
+ observedAt
4801
+ };
4802
+ }
4803
+ function errorState(attempt, code, message) {
4804
+ return {
4805
+ state: "error",
4806
+ data: null,
4807
+ attempt,
4808
+ error: {
4809
+ code,
4810
+ message
4811
+ },
4812
+ observedAt: attempt.observedAt
4813
+ };
4814
+ }
4815
+ function rootsMatch(doctor, context) {
4816
+ return doctor.path === context.path && doctor.source === context.source && (doctor.store_id === void 0 || context.store_id === void 0 || doctor.store_id === context.store_id);
4817
+ }
4818
+ function classifyRootContext(attempt, doctor, context) {
4819
+ const { cli, planningRoot, references } = attempt;
4820
+ if (!cli.available) return errorState(attempt, "cli-unavailable", cli.error ?? "OpenSpec CLI is unavailable.");
4821
+ if (!doctor) return errorState(attempt, "resolver-failed", "OpenSpec Doctor did not return command evidence.");
4822
+ if (doctor.contractError) return errorState(attempt, "doctor-contract-drift", commandMessage(doctor, "OpenSpec Doctor returned an incompatible payload."));
4823
+ if (!doctor.success) return errorState(attempt, "doctor-command-failed", commandMessage(doctor, "OpenSpec Doctor failed."));
4824
+ if (!planningRoot) return errorState(attempt, "root-unresolved", "OpenSpec Doctor did not resolve a planning root.");
4825
+ if (!planningRoot.healthy) return errorState(attempt, "root-unhealthy", planningRoot.status.map((diagnostic) => diagnostic.message).join("\n") || "OpenSpec Doctor reported an unhealthy planning root.");
4826
+ if (!context) return errorState(attempt, "resolver-failed", "OpenSpec Context did not return command evidence.");
4827
+ if (context.contractError) return errorState(attempt, "context-contract-drift", commandMessage(context, "OpenSpec Context returned an incompatible payload."));
4828
+ if (!context.success) return errorState(attempt, "context-command-failed", commandMessage(context, "OpenSpec Context failed."));
4829
+ if (!context.data?.root || !rootsMatch(planningRoot, context.data.root)) return errorState(attempt, "context-root-mismatch", "OpenSpec Doctor and Context did not report the same planning root.");
4830
+ const failedReference = references.flatMap((reference) => reference.status).find((diagnostic) => diagnostic.severity === "error");
4831
+ if (failedReference) return errorState(attempt, "references-unresolved", failedReference.message);
4832
+ return {
4833
+ state: "ready",
4834
+ data: attempt,
4835
+ attempt: null,
4836
+ error: null,
4837
+ observedAt: attempt.observedAt
4838
+ };
4839
+ }
4840
+ function unavailableCli(error) {
4841
+ return {
4842
+ available: false,
4843
+ error: error instanceof Error ? error.message : String(error)
4844
+ };
4845
+ }
4846
+ /** Resolve one current Root Context attempt from official CLI projections and process scope. */
4847
+ async function resolveRootContext(options) {
4848
+ const observedAt = options.now?.() ?? Date.now();
4849
+ let cli;
4850
+ try {
4851
+ cli = await options.cliExecutor.checkAvailability();
4852
+ } catch (error) {
4853
+ cli = unavailableCli(error);
4854
+ }
4855
+ if (!cli.available) return classifyRootContext(createRootContext(options, cli, null, null, observedAt), null, null);
4856
+ const selector = {};
4857
+ const [doctorSettled, contextSettled] = await Promise.allSettled([options.cliExecutor.contracts.doctorRoot(selector), options.cliExecutor.contracts.context(selector)]);
4858
+ const doctor = doctorSettled.status === "fulfilled" ? doctorSettled.value : null;
4859
+ const context = contextSettled.status === "fulfilled" ? contextSettled.value : null;
4860
+ const attempt = createRootContext(options, cli, doctor, context, observedAt);
4861
+ if (doctorSettled.status === "rejected" || contextSettled.status === "rejected") {
4862
+ let failure;
4863
+ if (doctorSettled.status === "rejected") failure = doctorSettled.reason;
4864
+ else if (contextSettled.status === "rejected") failure = contextSettled.reason;
4865
+ return errorState(attempt, "resolver-failed", failure instanceof Error ? failure.message : String(failure));
4866
+ }
4867
+ return classifyRootContext(attempt, doctor, context);
4868
+ }
4869
+
4870
+ //#endregion
4871
+ //#region src/runtime-root-invalidation.ts
4872
+ /**
4873
+ * Orthogonal intents (created 2026-07-16 Asia/Shanghai):
4874
+ * 1. Map dynamic filesystem roots to selected runtime invalidation facets.
4875
+ * 2. Share one recursive path subscription for repeated normalized roots.
4876
+ * 3. Release root subscriptions after their final logical owner or registry teardown.
4877
+ *
4878
+ * Original request (2026-07-15): "Launch-project and connected planning-root changes invalidate their project/context facets."
4879
+ */
4880
+ /** Reference-counted path-to-facet mapping; physical watcher roots remain separately owned. */
4881
+ var RuntimeRootInvalidationRegistry = class {
4882
+ records = /* @__PURE__ */ new Map();
4883
+ constructor(invalidation, facets) {
4884
+ this.invalidation = invalidation;
4885
+ this.facets = facets;
4886
+ }
4887
+ acquireRoot(rootPath) {
4888
+ const normalizedRoot = resolveRealPathThroughExistingAncestor(rootPath);
4889
+ let record = this.records.get(normalizedRoot);
4890
+ if (!record) {
4891
+ record = {
4892
+ rootPath: normalizedRoot,
4893
+ referenceCount: 0,
4894
+ releasePath: acquireWatcher(normalizedRoot, () => this.invalidation.invalidate(this.facets), { recursive: true })
4895
+ };
4896
+ this.records.set(normalizedRoot, record);
4897
+ }
4898
+ record.referenceCount += 1;
4899
+ let released = false;
4125
4900
  return () => {
4126
- child.kill();
4901
+ if (released) return;
4902
+ released = true;
4903
+ const current = this.records.get(normalizedRoot);
4904
+ if (current !== record || current.referenceCount === 0) return;
4905
+ current.referenceCount -= 1;
4906
+ if (current.referenceCount > 0) return;
4907
+ current.releasePath();
4908
+ this.records.delete(normalizedRoot);
4127
4909
  };
4128
4910
  }
4911
+ getRoots() {
4912
+ return [...this.records.values()].map(({ rootPath, referenceCount }) => ({
4913
+ rootPath,
4914
+ referenceCount
4915
+ })).sort((left, right) => left.rootPath.localeCompare(right.rootPath));
4916
+ }
4917
+ dispose() {
4918
+ const records = [...this.records.values()];
4919
+ this.records.clear();
4920
+ for (const record of records) record.releasePath();
4921
+ }
4129
4922
  };
4130
4923
 
4131
4924
  //#endregion
4132
4925
  //#region src/tool-config.ts
4133
4926
  /**
4927
+ * Orthogonal intents (updated 2026-07-15 Asia/Shanghai):
4928
+ * 1. Mirror official OpenSpec AI tool metadata and generated skill names.
4929
+ * 2. Detect configured project-local AI tool roots reactively.
4930
+ *
4931
+ * Original request (2026-07-15): "Oh My Pi、Trae 的完整交付链。"
4932
+ *
4134
4933
  * 工具配置检测模块
4135
4934
  *
4136
4935
  * 对齐 @fission-ai/openspec 的 skills 体系,
@@ -4145,6 +4944,7 @@ const SKILL_NAMES = [
4145
4944
  "openspec-new-change",
4146
4945
  "openspec-continue-change",
4147
4946
  "openspec-apply-change",
4947
+ "openspec-update-change",
4148
4948
  "openspec-ff-change",
4149
4949
  "openspec-sync-specs",
4150
4950
  "openspec-archive-change",
@@ -4314,6 +5114,27 @@ const AI_TOOLS = [
4314
5114
  successLabel: "Kiro",
4315
5115
  skillsDir: ".kiro"
4316
5116
  },
5117
+ {
5118
+ name: "Lingma",
5119
+ value: "lingma",
5120
+ available: true,
5121
+ successLabel: "Lingma",
5122
+ skillsDir: ".lingma"
5123
+ },
5124
+ {
5125
+ name: "Mistral Vibe",
5126
+ value: "vibe",
5127
+ available: true,
5128
+ successLabel: "Mistral Vibe",
5129
+ skillsDir: ".vibe"
5130
+ },
5131
+ {
5132
+ name: "Oh My Pi",
5133
+ value: "oh-my-pi",
5134
+ available: true,
5135
+ successLabel: "Oh My Pi",
5136
+ skillsDir: ".omp"
5137
+ },
4317
5138
  {
4318
5139
  name: "OpenCode",
4319
5140
  value: "opencode",
@@ -4335,20 +5156,6 @@ const AI_TOOLS = [
4335
5156
  successLabel: "Qoder",
4336
5157
  skillsDir: ".qoder"
4337
5158
  },
4338
- {
4339
- name: "Lingma",
4340
- value: "lingma",
4341
- available: true,
4342
- successLabel: "Lingma",
4343
- skillsDir: ".lingma"
4344
- },
4345
- {
4346
- name: "Mistral Vibe",
4347
- value: "vibe",
4348
- available: true,
4349
- successLabel: "Mistral Vibe",
4350
- skillsDir: ".vibe"
4351
- },
4352
5159
  {
4353
5160
  name: "Qwen Code",
4354
5161
  value: "qwen",
@@ -4491,22 +5298,23 @@ async function isToolConfigured(projectDir, toolId) {
4491
5298
 
4492
5299
  //#endregion
4493
5300
  //#region src/tool-init-state.ts
4494
- const TOOL_WORKFLOW_TO_SKILL_DIR = {
4495
- propose: "openspec-propose",
4496
- explore: "openspec-explore",
4497
- new: "openspec-new-change",
4498
- continue: "openspec-continue-change",
4499
- apply: "openspec-apply-change",
4500
- ff: "openspec-ff-change",
4501
- sync: "openspec-sync-specs",
4502
- archive: "openspec-archive-change",
4503
- "bulk-archive": "openspec-bulk-archive-change",
4504
- verify: "openspec-verify-change",
4505
- onboard: "openspec-onboard"
4506
- };
4507
- const ALL_TOOL_WORKFLOWS = Object.keys(TOOL_WORKFLOW_TO_SKILL_DIR);
5301
+ /**
5302
+ * Orthogonal intents (updated 2026-07-28 Asia/Shanghai):
5303
+ * 1. Map official OPSX workflows to generated skill and command locations.
5304
+ * 2. Project launch-local skills and physically scoped tool command initialization state.
5305
+ * 3. Provide the runtime owner with the same external Codex command root used by command projection.
5306
+ * 4. Bound reactive observation through existing parent inventories while preserving fresh one-shot reads.
5307
+ *
5308
+ * Original request (2026-07-15): "sync、update、Oh My Pi、Trae 的完整交付链。"
5309
+ * Remote CI fixed point (2026-07-28): Launch-local skill creation must converge when its tool root was absent at initial projection.
5310
+ */
5311
+ /** Return the external runtime directory containing official Codex command prompts. */
5312
+ function getExternalCodexCommandObservationRoot() {
5313
+ return join$1(resolveCodexHome(), "prompts");
5314
+ }
5315
+ const ALL_TOOL_WORKFLOWS = [...OPSX_ALL_WORKFLOWS];
4508
5316
  function toKnownWorkflows(workflows) {
4509
- return workflows.filter((workflow) => workflow in TOOL_WORKFLOW_TO_SKILL_DIR);
5317
+ return workflows.filter((workflow) => workflow in OPSX_WORKFLOW_TO_SKILL_DIR);
4510
5318
  }
4511
5319
  function resolveCodexHome() {
4512
5320
  const configuredHome = process.env.CODEX_HOME?.trim();
@@ -4537,10 +5345,12 @@ const TOOL_COMMAND_PATHS = {
4537
5345
  primary: (projectDir, workflow) => resolve(projectDir, ".opencode", "commands", `opsx-${workflow}.md`),
4538
5346
  legacy: [(projectDir, workflow) => resolve(projectDir, ".opencode", "command", `opsx-${workflow}.md`)]
4539
5347
  },
5348
+ "oh-my-pi": { primary: (projectDir, workflow) => resolve(projectDir, ".omp", "commands", `opsx-${workflow}.md`) },
4540
5349
  pi: { primary: (projectDir, workflow) => resolve(projectDir, ".pi", "prompts", `opsx-${workflow}.md`) },
4541
5350
  qoder: { primary: (projectDir, workflow) => resolve(projectDir, ".qoder", "commands", "opsx", `${workflow}.md`) },
4542
5351
  qwen: { primary: (projectDir, workflow) => resolve(projectDir, ".qwen", "commands", `opsx-${workflow}.toml`) },
4543
5352
  roocode: { primary: (projectDir, workflow) => resolve(projectDir, ".roo", "commands", `opsx-${workflow}.md`) },
5353
+ trae: { primary: (projectDir, workflow) => resolve(projectDir, ".trae", "commands", `opsx-${workflow}.md`) },
4544
5354
  windsurf: { primary: (projectDir, workflow) => resolve(projectDir, ".windsurf", "workflows", `opsx-${workflow}.md`) }
4545
5355
  };
4546
5356
  function resolveToolCommandArtifact(projectDir, toolId, workflow) {
@@ -4555,7 +5365,7 @@ function resolveToolCommandArtifact(projectDir, toolId, workflow) {
4555
5365
  function getSkillArtifacts(projectDir, skillsDir) {
4556
5366
  return ALL_TOOL_WORKFLOWS.map((workflow) => ({
4557
5367
  workflow,
4558
- path: resolve(projectDir, skillsDir, "skills", TOOL_WORKFLOW_TO_SKILL_DIR[workflow], "SKILL.md")
5368
+ path: resolve(projectDir, skillsDir, "skills", OPSX_WORKFLOW_TO_SKILL_DIR[workflow], "SKILL.md")
4559
5369
  }));
4560
5370
  }
4561
5371
  function getCommandArtifacts(projectDir, toolId) {
@@ -4565,7 +5375,7 @@ function getCommandArtifacts(projectDir, toolId) {
4565
5375
  });
4566
5376
  }
4567
5377
  function invalidateToolInitCaches(projectDir) {
4568
- const cacheRoots = /* @__PURE__ */ new Set();
5378
+ const cacheRoots = new Set([resolve(projectDir)]);
4569
5379
  for (const tool of AI_TOOLS) {
4570
5380
  if (tool.skillsDir) cacheRoots.add(resolve(projectDir, tool.skillsDir));
4571
5381
  for (const commandArtifact of getCommandArtifacts(projectDir, tool.value)) {
@@ -4575,13 +5385,34 @@ function invalidateToolInitCaches(projectDir) {
4575
5385
  }
4576
5386
  for (const root of cacheRoots) clearCache(root);
4577
5387
  }
4578
- async function getExistingArtifactPaths(entries) {
4579
- const paths = entries.flatMap((entry) => [entry.path, ...entry.legacyPaths ?? []]);
4580
- const presence = await Promise.all(paths.map(async (path) => ({
4581
- path,
4582
- exists: await reactiveExists(path)
5388
+ async function readArtifactDirectory(dir) {
5389
+ return new Set(await reactiveReadDir(dir, { includeHidden: true }));
5390
+ }
5391
+ async function getExistingSkillPaths(projectDir, skillsDir, entries, projectRootEntries) {
5392
+ const toolRoot = resolve(projectDir, skillsDir);
5393
+ if (!projectRootEntries.has(basename(toolRoot))) return /* @__PURE__ */ new Set();
5394
+ if (!(await readArtifactDirectory(toolRoot)).has("skills")) return /* @__PURE__ */ new Set();
5395
+ const rootEntries = await readArtifactDirectory(resolve(toolRoot, "skills"));
5396
+ const presentEntries = entries.filter((entry) => rootEntries.has(basename(dirname$1(entry.path))));
5397
+ const directoryEntries = await Promise.all(presentEntries.map(async (entry) => ({
5398
+ entry,
5399
+ files: await readArtifactDirectory(dirname$1(entry.path))
5400
+ })));
5401
+ return new Set(directoryEntries.filter(({ entry, files }) => files.has(basename(entry.path))).map(({ entry }) => entry.path));
5402
+ }
5403
+ async function getExistingCommandPaths(entries) {
5404
+ const pathsByDirectory = /* @__PURE__ */ new Map();
5405
+ for (const entry of entries) for (const path of [entry.path, ...entry.legacyPaths ?? []]) {
5406
+ const directory = dirname$1(path);
5407
+ const paths = pathsByDirectory.get(directory) ?? [];
5408
+ paths.push(path);
5409
+ pathsByDirectory.set(directory, paths);
5410
+ }
5411
+ const directoryEntries = await Promise.all([...pathsByDirectory].map(async ([directory, paths]) => ({
5412
+ paths,
5413
+ files: await readArtifactDirectory(directory)
4583
5414
  })));
4584
- return new Set(presence.filter((entry) => entry.exists).map((entry) => entry.path));
5415
+ return new Set(directoryEntries.flatMap(({ paths, files }) => paths.filter((path) => files.has(basename(path)))));
4585
5416
  }
4586
5417
  function hasExistingArtifact(entry, existingPaths) {
4587
5418
  return existingPaths.has(entry.path) || (entry.legacyPaths?.some((legacyPath) => existingPaths.has(legacyPath)) ?? false);
@@ -4601,17 +5432,17 @@ function collectUnexpectedWorkflows(entries, desiredWorkflowSet, existingPaths)
4601
5432
  function collectLegacyWorkflows(entries, existingPaths) {
4602
5433
  return entries.filter((entry) => hasLegacyArtifact(entry, existingPaths)).map((entry) => entry.workflow);
4603
5434
  }
4604
- async function getToolInitStates(projectDir, options) {
4605
- invalidateToolInitCaches(projectDir);
5435
+ async function projectToolInitStates(projectDir, options) {
4606
5436
  const desiredWorkflows = toKnownWorkflows(options.workflows);
4607
5437
  const desiredWorkflowSet = new Set(desiredWorkflows);
4608
5438
  const shouldGenerateSkills = options.delivery !== "commands";
4609
5439
  const shouldGenerateCommands = options.delivery !== "skills";
5440
+ const projectRootEntries = await readArtifactDirectory(projectDir);
4610
5441
  return Promise.all(AI_TOOLS.filter((tool) => tool.skillsDir).map(async (tool) => {
4611
5442
  const skillArtifacts = getSkillArtifacts(projectDir, tool.skillsDir);
4612
5443
  const commandArtifacts = getCommandArtifacts(projectDir, tool.value);
4613
- const existingSkillPaths = await getExistingArtifactPaths(skillArtifacts);
4614
- const existingCommandPaths = await getExistingArtifactPaths(commandArtifacts);
5444
+ const existingSkillPaths = await getExistingSkillPaths(projectDir, tool.skillsDir, skillArtifacts, projectRootEntries);
5445
+ const existingCommandPaths = await getExistingCommandPaths(commandArtifacts);
4615
5446
  const expectedSkillArtifacts = shouldGenerateSkills ? skillArtifacts.filter((entry) => desiredWorkflowSet.has(entry.workflow)) : [];
4616
5447
  const expectedCommandArtifacts = shouldGenerateCommands ? commandArtifacts.filter((entry) => desiredWorkflowSet.has(entry.workflow)) : [];
4617
5448
  const missingSkillWorkflows = collectMissingWorkflows(expectedSkillArtifacts, existingSkillPaths);
@@ -4646,165 +5477,167 @@ async function getToolInitStates(projectDir, options) {
4646
5477
  };
4647
5478
  }));
4648
5479
  }
4649
-
4650
- //#endregion
4651
- //#region src/dashboard-types.ts
4652
- const DASHBOARD_METRIC_KEYS = [
4653
- "specifications",
4654
- "requirements",
4655
- "activeChanges",
4656
- "inProgressChanges",
4657
- "completedChanges",
4658
- "taskCompletionPercent"
4659
- ];
5480
+ /** Read a fresh one-shot Project Tool artifact inventory. */
5481
+ async function getToolInitStates(projectDir, options) {
5482
+ invalidateToolInitCaches(projectDir);
5483
+ return projectToolInitStates(projectDir, options);
5484
+ }
5485
+ /** Create a retained Tool inventory task that must be executed by one ReactiveContext. */
5486
+ function createToolInitStateProjection(projectDir, options) {
5487
+ const projectionOptions = {
5488
+ delivery: options.delivery,
5489
+ workflows: [...options.workflows]
5490
+ };
5491
+ return () => projectToolInitStates(projectDir, projectionOptions);
5492
+ }
4660
5493
 
4661
5494
  //#endregion
4662
5495
  //#region src/hooks.ts
4663
- const OPENSPECUI_HOOKS_VERSION = 1;
5496
+ const OPENSPECUI_DOCUMENT_HOOK_VERSION = 1;
5497
+ const OPENSPECUI_WORKFLOW_HOOK_VERSION = 2;
4664
5498
 
4665
5499
  //#endregion
4666
- //#region src/opsx-types.ts
4667
- /** Check if an outputPath contains glob pattern characters */
4668
- function isGlobPattern(pattern) {
4669
- return pattern.includes("*") || pattern.includes("?") || pattern.includes("[");
4670
- }
4671
- const ArtifactStatusSchema = z.object({
4672
- id: z.string(),
4673
- outputPath: z.string(),
4674
- status: z.enum([
4675
- "done",
4676
- "ready",
4677
- "blocked"
4678
- ]),
4679
- missingDeps: z.array(z.string()).optional(),
4680
- relativePath: z.string().optional()
4681
- });
4682
- const ChangeStatusSchema = z.object({
4683
- changeName: z.string(),
4684
- schemaName: z.string(),
4685
- isComplete: z.boolean(),
4686
- applyRequires: z.array(z.string()),
4687
- artifacts: z.array(ArtifactStatusSchema)
4688
- });
4689
- const DependencyInfoSchema = z.object({
4690
- id: z.string(),
4691
- done: z.boolean(),
4692
- path: z.string(),
4693
- description: z.string()
4694
- });
4695
- const ApplyTaskSchema = z.object({
4696
- id: z.string(),
4697
- description: z.string(),
4698
- done: z.boolean()
4699
- });
4700
- const ApplyInstructionsContextFilePathsSchema = z.union([z.string(), z.array(z.string())]).transform((paths) => Array.isArray(paths) ? paths : [paths]);
4701
- const ApplyInstructionsContextFilesSchema = z.record(ApplyInstructionsContextFilePathsSchema);
4702
- const ApplyInstructionsSchema = z.object({
4703
- changeName: z.string(),
4704
- changeDir: z.string(),
4705
- schemaName: z.string(),
4706
- contextFiles: ApplyInstructionsContextFilesSchema,
4707
- progress: z.object({
4708
- total: z.number(),
4709
- complete: z.number(),
4710
- remaining: z.number()
4711
- }),
4712
- tasks: z.array(ApplyTaskSchema),
4713
- state: z.enum([
4714
- "blocked",
4715
- "all_done",
4716
- "ready"
4717
- ]),
4718
- missingArtifacts: z.array(z.string()).optional(),
4719
- instruction: z.string()
4720
- });
4721
- const NullableString = z.string().nullable().optional();
4722
- const ArtifactInstructionsSchema = z.object({
4723
- changeName: z.string(),
4724
- artifactId: z.string(),
4725
- schemaName: z.string(),
4726
- changeDir: z.string(),
4727
- outputPath: z.string(),
4728
- description: z.string(),
4729
- instruction: NullableString,
4730
- context: NullableString,
4731
- rules: z.array(z.string()).optional().nullable(),
4732
- template: z.string(),
4733
- dependencies: z.array(DependencyInfoSchema),
4734
- unlocks: z.array(z.string())
4735
- });
4736
- const SchemaInfoSchema = z.object({
4737
- name: z.string(),
4738
- description: z.string().optional(),
4739
- artifacts: z.array(z.string()),
4740
- source: z.enum([
4741
- "project",
4742
- "user",
4743
- "package"
4744
- ])
4745
- });
4746
- const SchemaResolutionSchema = z.object({
4747
- name: z.string(),
4748
- source: z.enum([
4749
- "project",
4750
- "user",
4751
- "package"
4752
- ]),
4753
- path: z.string(),
4754
- displayPath: z.string().optional(),
4755
- shadows: z.array(z.object({
4756
- source: z.enum([
4757
- "project",
4758
- "user",
4759
- "package"
4760
- ]),
4761
- path: z.string(),
4762
- displayPath: z.string().optional()
4763
- }))
4764
- });
4765
- const TemplatesSchema = z.record(z.object({
4766
- path: z.string(),
4767
- displayPath: z.string().optional(),
4768
- source: z.enum([
4769
- "project",
4770
- "user",
4771
- "package"
4772
- ])
4773
- }));
4774
- const SchemaArtifactSchema = z.object({
4775
- id: z.string(),
4776
- outputPath: z.string(),
4777
- description: z.string().optional(),
4778
- template: z.string().optional(),
4779
- instruction: z.string().optional(),
4780
- requires: z.array(z.string())
4781
- });
4782
- const SchemaDetailSchema = z.object({
4783
- name: z.string(),
4784
- description: z.string().optional(),
4785
- version: z.union([z.string(), z.number()]).optional(),
4786
- artifacts: z.array(SchemaArtifactSchema),
4787
- applyRequires: z.array(z.string()),
4788
- applyTracks: z.string().optional(),
4789
- applyInstruction: z.string().optional()
4790
- });
5500
+ //#region src/hosted-protocol.ts
5501
+ /**
5502
+ * Orthogonal intents (updated 2026-07-25 Asia/Shanghai):
5503
+ * 1. Compute opaque environment identity and Access Gate credentials with Node crypto.
5504
+ * 2. Re-export browser-safe hosted identity/capability/mutation facts without making this Node entry browser-safe.
5505
+ *
5506
+ * Original request (2026-07-15): "我们可以在 cli 上新增一个 --auth 或者 --password。"
5507
+ * Migrated from `packages/app/src/types/{env-uri,capabilities,store-mutation}.ts` so the backend is the
5508
+ * authoritative producer and both App and Server share one typed contract.
5509
+ *
5510
+ * Key invariants (AGENTS.md):
5511
+ * - `envUri` is backend-issued, opaque, non-dereferenceable; it identifies host identity + effective
5512
+ * OpenSpec data home. Multiple processes/ports/projects share one envUri when that pair is unchanged.
5513
+ * - Capabilities are objective compatibility facts, never permissions, authorization, or workflow state.
5514
+ * - Store mutations are backend-owned: `accepted -> running -> succeeded | failed`; unrecoverable
5515
+ * terminal loss is `indeterminate`, never fabricated as failure or cancellation. V1 has no Cancel
5516
+ * and no automatic retry. A client request id deduplicates starts within one backend process.
5517
+ * - The Access Gate is one shared Bearer credential for the whole backend boundary; it is not an
5518
+ * account, role, ACL, or permission system, and it provides no transport confidentiality.
5519
+ */
5520
+ /**
5521
+ * Compute a stable opaque `envUri` from backend host identity plus effective OpenSpec data home.
5522
+ * The inputs are hashed so the returned URI never exposes either component; only pair equality matters.
5523
+ */
5524
+ function computeEnvUri(options) {
5525
+ const dataHome = options.dataHome ?? "";
5526
+ return asEnvUri(`openspecui-env://1/${createHash("sha256").update(`${options.hostIdentity}\u0000${dataHome}`).digest("hex")}`);
5527
+ }
5528
+ /** Required byte length of a generated `--auth` credential (256 bits of entropy). */
5529
+ const ACCESS_GATE_CREDENTIAL_BYTES = 32;
5530
+ /** Base64url-encode without padding. */
5531
+ function base64url(bytes) {
5532
+ return Buffer.from(bytes).toString("base64url");
5533
+ }
5534
+ /** Compute a short SHA-256 fingerprint suitable for safe display. */
5535
+ function accessGateFingerprint(credential) {
5536
+ return createHash("sha256").update(credential).digest("hex").slice(0, 16);
5537
+ }
5538
+ /** Generate a high-entropy Bearer credential for `--auth` and return the complete gate record. */
5539
+ function generateAccessGateCredential() {
5540
+ const credential = base64url(randomBytes(ACCESS_GATE_CREDENTIAL_BYTES));
5541
+ return {
5542
+ credential,
5543
+ fingerprint: accessGateFingerprint(credential),
5544
+ authorizationHeader: `Bearer ${credential}`
5545
+ };
5546
+ }
5547
+ /**
5548
+ * Normalize an operator-supplied `--password` secret into the same Bearer gate record. The secret is
5549
+ * used verbatim as the credential (no hashing) so the gate is a single shared secret comparison.
5550
+ */
5551
+ function normalizeAccessGatePassword(password) {
5552
+ const credential = password;
5553
+ return {
5554
+ credential,
5555
+ fingerprint: accessGateFingerprint(credential),
5556
+ authorizationHeader: `Bearer ${credential}`
5557
+ };
5558
+ }
5559
+ /** Constant-time comparison of two equal-length secrets to mitigate timing side channels. */
5560
+ function constantTimeEqual(a, b) {
5561
+ const left = Buffer.from(a);
5562
+ const right = Buffer.from(b);
5563
+ if (left.length !== right.length) {
5564
+ left.compare(left);
5565
+ return false;
5566
+ }
5567
+ return left.compare(right) === 0;
5568
+ }
4791
5569
 
4792
5570
  //#endregion
4793
- //#region src/opsx-kernel.ts
4794
- function parseCliJson(raw, schema, label) {
4795
- const trimmed = raw.trim();
4796
- if (!trimmed) throw new Error(`${label} returned empty output`);
4797
- let parsed;
4798
- try {
4799
- parsed = JSON.parse(trimmed);
4800
- } catch (err) {
4801
- const message = err instanceof Error ? err.message : String(err);
4802
- throw new Error(`${label} returned invalid JSON: ${message}`);
5571
+ //#region src/snapshot-redaction.ts
5572
+ function isAbsoluteFsPath$1(path) {
5573
+ const normalized = path.replace(/\\/g, "/");
5574
+ return normalized.startsWith("/") || /^[A-Za-z]:\//.test(normalized);
5575
+ }
5576
+ /**
5577
+ * Redact an absolute path to a display-safe relative fragment. Absolute filesystem locations must
5578
+ * never survive into a published snapshot; only the trailing relative segment is retained so a reader
5579
+ * still understands the artifact's repository-relative location.
5580
+ */
5581
+ function redactPath(raw) {
5582
+ if (!raw) return raw;
5583
+ const normalized = raw.replace(/\\/g, "/");
5584
+ if (!isAbsoluteFsPath$1(normalized)) return normalized;
5585
+ return normalized.split("/").filter(Boolean).slice(-2).join("/");
5586
+ }
5587
+ /** Deep-clone helper that preserves JSON-serializable snapshot shape without mutating input. */
5588
+ function cloneSnapshot(snapshot) {
5589
+ return JSON.parse(JSON.stringify(snapshot));
5590
+ }
5591
+ /**
5592
+ * Produce a publication-safe copy of `snapshot`. Removes absolute project/Store/data-home/registry
5593
+ * paths, host identity, and raw path-bearing diagnostics, and clears the Git remote URL unless the
5594
+ * Reference export policy is explicitly `include`. Display-safe relative paths (`displayPath`,
5595
+ * repository-relative fragments) and human-readable labels are preserved.
5596
+ *
5597
+ * This is the single redaction boundary for static export privacy (Section 7.8). Callers must run it
5598
+ * immediately before serializing the published `data.json` / SSG site.
5599
+ */
5600
+ function redactSnapshotForPublication(snapshot) {
5601
+ const redacted = cloneSnapshot(snapshot);
5602
+ const root = redacted.meta.root;
5603
+ if (root && root.planningRootPath) root.planningRootPath = redactPath(root.planningRootPath);
5604
+ if (redacted.git && redacted.meta.referencePolicy?.kind !== "include") redacted.git.repositoryUrl = null;
5605
+ const opsx = redacted.opsx;
5606
+ if (opsx) {
5607
+ for (const resolution of Object.values(opsx.schemaResolutions)) {
5608
+ if (typeof resolution.path === "string") resolution.path = redactPath(resolution.path);
5609
+ for (const shadow of resolution.shadows ?? []) if (typeof shadow.path === "string") shadow.path = redactPath(shadow.path);
5610
+ }
5611
+ for (const artifactMap of Object.values(opsx.templates)) for (const info of Object.values(artifactMap)) if (typeof info.path === "string") info.path = redactPath(info.path);
5612
+ if (opsx.templateContents) {
5613
+ for (const artifactMap of Object.values(opsx.templateContents)) for (const entry of Object.values(artifactMap)) if (typeof entry.path === "string") entry.path = redactPath(entry.path);
5614
+ }
4803
5615
  }
4804
- const result = schema.safeParse(parsed);
4805
- if (!result.success) throw new Error(`${label} returned unexpected JSON: ${result.error.message}`);
4806
- return result.data;
5616
+ if (opsx?.changeMetadata) {
5617
+ for (const [id, raw] of Object.entries(opsx.changeMetadata)) if (typeof raw === "string") opsx.changeMetadata[id] = raw.replace(/(?:\/[\w.\-@]+){2,}(?=\s|"|$)/g, (match) => redactPath(match));
5618
+ }
5619
+ return redacted;
5620
+ }
5621
+ /** True when any string field in the snapshot still carries an absolute filesystem path. */
5622
+ function snapshotHasAbsolutePath(snapshot) {
5623
+ const text = JSON.stringify(snapshot);
5624
+ return /\b(?:\/[\w.\-@]+){2,}/.test(text) || /[A-Za-z]:\//.test(text);
4807
5625
  }
5626
+
5627
+ //#endregion
5628
+ //#region src/opsx-kernel.ts
5629
+ /**
5630
+ * Orthogonal intents (updated 2026-07-27 Asia/Shanghai):
5631
+ * 1. Maintain reactive CLI-backed projections and their direct typed Projection Work readers.
5632
+ * 2. Share and release per-entity streams through one planning-root kernel lifecycle.
5633
+ * 3. Keep OpenSpec configuration ownership outside the workflow projection cache.
5634
+ * 4. Preserve typed Status/Instructions provenance with the resolved root selector.
5635
+ * 5. Keep Change enumeration business truth in the typed CLI result while files only invalidate it.
5636
+ *
5637
+ * Original request (2026-07-15): "Planning-root adapters and services consume the CLI-resolved root."
5638
+ * Original request (2026-07-23): "OPSX Status 不应等待完整 Kernel warmup,且必须保留 CLI evidence。"
5639
+ * Original request (2026-07-26): "展开全面的接口升级和内核升级和测试升级。"
5640
+ */
4808
5641
  function toRelativePath(root, absolutePath) {
4809
5642
  return relative(root, absolutePath).split(sep).join("/");
4810
5643
  }
@@ -4849,6 +5682,12 @@ async function readEntriesUnderRoot(root) {
4849
5682
  };
4850
5683
  return collectEntries(root);
4851
5684
  }
5685
+ function requireCanonicalArtifactLocation(changeId, outputPath) {
5686
+ return {
5687
+ changeId: requireCanonicalOpenSpecEntityId(changeId, "changeId"),
5688
+ outputPath: requireOpenSpecEntityRelativePath(outputPath, "outputPath")
5689
+ };
5690
+ }
4852
5691
  function splitRelativePathSegments(path) {
4853
5692
  return path.replace(/\\/g, "/").split("/").filter(Boolean);
4854
5693
  }
@@ -4861,7 +5700,8 @@ function getGlobStaticPrefix(outputPath) {
4861
5700
  return lastSlashIndex === -1 ? "" : staticPrefix.slice(0, lastSlashIndex);
4862
5701
  }
4863
5702
  async function readGlobArtifactFiles(projectDir, changeId, outputPath) {
4864
- return (await readEntriesUnderRoot(join$1(projectDir, "openspec", "changes", changeId))).filter((entry) => entry.type === "file" && matchesGlob(entry.path, outputPath)).map((entry) => ({
5703
+ const location = requireCanonicalArtifactLocation(changeId, outputPath);
5704
+ return (await readEntriesUnderRoot(join$1(projectDir, "openspec", "changes", location.changeId))).filter((entry) => entry.type === "file" && matchesGlob(entry.path, location.outputPath)).map((entry) => ({
4865
5705
  path: entry.path,
4866
5706
  type: "file",
4867
5707
  content: entry.content ?? ""
@@ -4904,8 +5744,9 @@ async function touchDirectoryTree(rootDir) {
4904
5744
  }));
4905
5745
  }
4906
5746
  async function touchArtifactOutputDeps(projectDir, changeId, outputPath) {
4907
- const changeDir = join$1(projectDir, "openspec", "changes", changeId);
4908
- const normalizedOutputPath = outputPath.replace(/\\/g, "/");
5747
+ const location = requireCanonicalArtifactLocation(changeId, outputPath);
5748
+ const changeDir = join$1(projectDir, "openspec", "changes", location.changeId);
5749
+ const normalizedOutputPath = location.outputPath;
4909
5750
  if (isGlobPattern(normalizedOutputPath)) {
4910
5751
  const staticPrefix = getGlobStaticPrefix(normalizedOutputPath);
4911
5752
  if (staticPrefix) await touchDirectoryPathDeps(changeDir, staticPrefix);
@@ -4916,16 +5757,44 @@ async function touchArtifactOutputDeps(projectDir, changeId, outputPath) {
4916
5757
  if (parentPath) await touchDirectoryPathDeps(changeDir, parentPath);
4917
5758
  await reactiveExists(join$1(changeDir, normalizedOutputPath));
4918
5759
  }
5760
+ function requireCommandData(label, result, schema) {
5761
+ const parsed = schema.safeParse(result.data);
5762
+ if (result.success && parsed.success) return parsed.data;
5763
+ throw new CliProjectionCommandError(result.contractError || result.stderr.trim() || result.diagnostics.map((diagnostic) => diagnostic.message).join("\n") || `${label} failed (exit ${result.exitCode ?? "null"})`, result);
5764
+ }
5765
+ function readOpsxRoot(data) {
5766
+ const parsed = z.object({ root: CliRootSchema }).passthrough().safeParse(data);
5767
+ return parsed.success ? parsed.data.root : void 0;
5768
+ }
5769
+ function createOpsxCliEvidence(result, command, rootSelector) {
5770
+ const root = readOpsxRoot(result.data);
5771
+ return OpsxCliEvidenceSchema.parse({
5772
+ command,
5773
+ success: result.success,
5774
+ stdout: result.stdout,
5775
+ stderr: result.stderr,
5776
+ exitCode: result.exitCode,
5777
+ payload: result.payload,
5778
+ diagnostics: result.diagnostics,
5779
+ ...result.contractError ? { contractError: result.contractError } : {},
5780
+ selector: rootSelector.store === void 0 ? {} : { store: rootSelector.store },
5781
+ ...root ? { root } : {}
5782
+ });
5783
+ }
5784
+ function createOpsxStatusEvidence(result, rootSelector) {
5785
+ return OpsxStatusEvidenceSchema.parse(createOpsxCliEvidence(result, "status", rootSelector));
5786
+ }
4919
5787
  var OpsxKernel = class {
4920
5788
  projectDir;
4921
5789
  cliExecutor;
5790
+ runtimeInvalidation;
5791
+ rootSelector;
4922
5792
  controller = new AbortController();
4923
5793
  warmupPromise = null;
4924
5794
  _streamReady = /* @__PURE__ */ new Map();
4925
5795
  _statusList = new ReactiveState([]);
4926
5796
  _schemas = new ReactiveState([]);
4927
5797
  _changeIds = new ReactiveState([]);
4928
- _projectConfig = new ReactiveState(null);
4929
5798
  _schemaResolutions = /* @__PURE__ */ new Map();
4930
5799
  _schemaDetails = /* @__PURE__ */ new Map();
4931
5800
  _schemaFiles = /* @__PURE__ */ new Map();
@@ -4939,9 +5808,11 @@ var OpsxKernel = class {
4939
5808
  _artifactOutputs = /* @__PURE__ */ new Map();
4940
5809
  _globArtifactFiles = /* @__PURE__ */ new Map();
4941
5810
  _entityControllers = /* @__PURE__ */ new Map();
4942
- constructor(projectDir, cliExecutor) {
5811
+ constructor(projectDir, cliExecutor, runtimeInvalidation, rootSelector) {
4943
5812
  this.projectDir = projectDir;
4944
5813
  this.cliExecutor = cliExecutor;
5814
+ this.runtimeInvalidation = runtimeInvalidation;
5815
+ this.rootSelector = rootSelector;
4945
5816
  }
4946
5817
  async warmup() {
4947
5818
  if (this.warmupPromise) return this.warmupPromise;
@@ -4956,11 +5827,7 @@ var OpsxKernel = class {
4956
5827
  }
4957
5828
  async runWarmup() {
4958
5829
  const signal = this.controller.signal;
4959
- await Promise.all([
4960
- this.startStreamOnce("global:schemas", this._schemas, () => this.fetchSchemas(), signal),
4961
- this.startStreamOnce("global:change-ids", this._changeIds, () => this.fetchChangeIds(), signal),
4962
- this.startStreamOnce("global:project-config", this._projectConfig, () => this.fetchProjectConfig(), signal)
4963
- ]);
5830
+ await Promise.all([this.startStreamOnce("global:schemas", this._schemas, () => this.fetchSchemas(), signal), this.startStreamOnce("global:change-ids", this._changeIds, () => this.fetchChangeIds(), signal)]);
4964
5831
  const schemas = this._schemas.get();
4965
5832
  await Promise.all(schemas.map((s) => this.warmupSchema(s.name, signal)));
4966
5833
  const changeIds = this._changeIds.get();
@@ -4985,9 +5852,6 @@ var OpsxKernel = class {
4985
5852
  getChangeIds() {
4986
5853
  return this._changeIds.get();
4987
5854
  }
4988
- getProjectConfig() {
4989
- return this._projectConfig.get();
4990
- }
4991
5855
  getTemplates(schema) {
4992
5856
  const key = schema ?? "";
4993
5857
  const state = this._templates.get(key);
@@ -5224,10 +6088,16 @@ var OpsxKernel = class {
5224
6088
  for (const key of this._streamReady.keys()) if (key.startsWith(prefix)) this._streamReady.delete(key);
5225
6089
  }
5226
6090
  async fetchSchemas() {
6091
+ return (await this.fetchSchemasProjection()).value;
6092
+ }
6093
+ async fetchSchemasProjection() {
6094
+ this.runtimeInvalidation.track("schemas");
5227
6095
  await touchOpsxProjectDeps(this.projectDir);
5228
- const result = await this.cliExecutor.schemas();
5229
- if (!result.success) throw new Error(result.stderr || `openspec schemas failed (exit ${result.exitCode ?? "null"})`);
5230
- return parseCliJson(result.stdout, z.array(SchemaInfoSchema), "openspec schemas");
6096
+ const result = await this.cliExecutor.contracts.schemas();
6097
+ return {
6098
+ value: requireCommandData("openspec schemas", result, CliSchemasSchema),
6099
+ evidence: toCliProjectionCommandEvidence(result)
6100
+ };
5231
6101
  }
5232
6102
  async fetchChangeIds() {
5233
6103
  return reactiveReadDir(join$1(this.projectDir, "openspec", "changes"), {
@@ -5236,22 +6106,40 @@ var OpsxKernel = class {
5236
6106
  exclude: ["archive"]
5237
6107
  });
5238
6108
  }
5239
- async fetchProjectConfig() {
5240
- return reactiveReadFile(join$1(this.projectDir, "openspec", "config.yaml"));
6109
+ async fetchChangeListProjection() {
6110
+ await this.fetchChangeIds();
6111
+ const result = await this.cliExecutor.contracts.listChanges(this.rootSelector);
6112
+ return {
6113
+ value: requireCommandData("openspec list", result, CliChangeListSchema).changes.map(({ name }) => name),
6114
+ evidence: toCliProjectionCommandEvidence(result)
6115
+ };
5241
6116
  }
5242
6117
  async fetchStatus(changeId, schema) {
5243
6118
  await touchOpsxProjectDeps(this.projectDir);
5244
6119
  await touchOpsxChangeDeps(this.projectDir, changeId);
5245
- const args = [
5246
- "status",
5247
- "--json",
5248
- "--change",
5249
- changeId
5250
- ];
5251
- if (schema) args.push("--schema", schema);
5252
- const result = await this.cliExecutor.execute(args);
5253
- if (!result.success) throw new Error(result.stderr || `openspec status failed (exit ${result.exitCode ?? "null"})`);
5254
- const status = parseCliJson(result.stdout, ChangeStatusSchema, "openspec status");
6120
+ const result = await this.cliExecutor.contracts.workflowStatus(changeId, {
6121
+ ...this.rootSelector,
6122
+ ...schema ? { schema } : {}
6123
+ });
6124
+ const evidence = createOpsxStatusEvidence(result, this.rootSelector);
6125
+ const data = requireCommandData("openspec status", result, CliWorkflowStatusSuccessSchema);
6126
+ const status = ChangeStatusSchema.parse({
6127
+ changeName: data.changeName,
6128
+ schemaName: data.schemaName,
6129
+ isComplete: data.isComplete,
6130
+ applyRequires: data.applyRequires,
6131
+ artifacts: data.artifacts,
6132
+ provenance: {
6133
+ kind: "cli",
6134
+ planningHome: data.planningHome,
6135
+ changeRoot: data.changeRoot,
6136
+ artifactPaths: data.artifactPaths,
6137
+ nextSteps: data.nextSteps,
6138
+ actionContext: data.actionContext,
6139
+ root: data.root,
6140
+ evidence
6141
+ }
6142
+ });
5255
6143
  const changeRelDir = `openspec/changes/${changeId}`;
5256
6144
  for (const artifact of status.artifacts) {
5257
6145
  artifact.relativePath = `${changeRelDir}/${artifact.outputPath}`;
@@ -5268,57 +6156,71 @@ var OpsxKernel = class {
5268
6156
  async fetchInstructions(changeId, artifact, schema) {
5269
6157
  await touchOpsxProjectDeps(this.projectDir);
5270
6158
  await touchOpsxChangeDeps(this.projectDir, changeId);
5271
- const args = [
5272
- "instructions",
5273
- artifact,
5274
- "--json",
5275
- "--change",
5276
- changeId
5277
- ];
5278
- if (schema) args.push("--schema", schema);
5279
- const result = await this.cliExecutor.execute(args);
5280
- if (!result.success) throw new Error(result.stderr || `openspec instructions failed (exit ${result.exitCode ?? "null"})`);
5281
- return parseCliJson(result.stdout, ArtifactInstructionsSchema, "openspec instructions");
6159
+ const result = await this.cliExecutor.contracts.artifactInstructions(changeId, artifact, {
6160
+ ...this.rootSelector,
6161
+ ...schema ? { schema } : {}
6162
+ });
6163
+ const data = requireCommandData("openspec instructions", result, CliArtifactInstructionsSuccessSchema);
6164
+ return ArtifactInstructionsSchema.parse({
6165
+ ...data,
6166
+ evidence: createOpsxCliEvidence(result, "instructions", this.rootSelector)
6167
+ });
5282
6168
  }
5283
6169
  async fetchApplyInstructions(changeId, schema) {
5284
6170
  await touchOpsxProjectDeps(this.projectDir);
5285
6171
  await touchOpsxChangeDeps(this.projectDir, changeId);
5286
- const args = [
5287
- "instructions",
5288
- "apply",
5289
- "--json",
5290
- "--change",
5291
- changeId
5292
- ];
5293
- if (schema) args.push("--schema", schema);
5294
- const result = await this.cliExecutor.execute(args);
5295
- if (!result.success) throw new Error(result.stderr || `openspec instructions apply failed (exit ${result.exitCode ?? "null"})`);
5296
- return parseCliJson(result.stdout, ApplyInstructionsSchema, "openspec instructions apply");
6172
+ const result = await this.cliExecutor.contracts.applyInstructions(changeId, {
6173
+ ...this.rootSelector,
6174
+ ...schema ? { schema } : {}
6175
+ });
6176
+ const data = requireCommandData("openspec instructions apply", result, CliApplyInstructionsSuccessSchema);
6177
+ const instructions = ApplyInstructionsSchema.parse({
6178
+ ...data,
6179
+ evidence: createOpsxCliEvidence(result, "instructions apply", this.rootSelector)
6180
+ });
6181
+ const changeDir = join$1(this.projectDir, "openspec", "changes", changeId);
6182
+ const [files, schemaDetail] = await Promise.all([readEntriesUnderRoot(changeDir), this.fetchSchemaResolution(instructions.schemaName).then((resolution) => this.fetchSchemaDetailAtResolution(instructions.schemaName, resolution)).catch(() => null)]);
6183
+ const { trackedTaskProgress } = projectTaskProjectionsFromMarkdownFiles(files, {
6184
+ schemaDetail,
6185
+ hasSchemaMetadata: true
6186
+ });
6187
+ return {
6188
+ ...instructions,
6189
+ applyInstructionProgress: createApplyInstructionProgress(instructions.applyInstructionProgress, trackedTaskProgress)
6190
+ };
5297
6191
  }
5298
6192
  async fetchSchemaResolution(name) {
6193
+ return (await this.fetchSchemaResolutionProjection(name)).value;
6194
+ }
6195
+ async fetchSchemaResolutionProjection(name) {
5299
6196
  await touchOpsxProjectDeps(this.projectDir);
5300
- const result = await this.cliExecutor.schemaWhich(name);
5301
- if (!result.success) throw new Error(result.stderr || `openspec schema which failed (exit ${result.exitCode ?? "null"})`);
5302
- const parsed = parseCliJson(result.stdout, SchemaResolutionSchema, "openspec schema which");
6197
+ const result = await this.cliExecutor.contracts.schemaWhich(name);
6198
+ const value = requireCommandData("openspec schema which", result, SchemaResolutionSchema);
5303
6199
  return {
5304
- ...parsed,
5305
- displayPath: toOpsxDisplayPath(parsed.path, {
5306
- source: parsed.source,
5307
- projectDir: this.projectDir
5308
- }),
5309
- shadows: parsed.shadows.map((shadow) => ({
5310
- ...shadow,
5311
- displayPath: toOpsxDisplayPath(shadow.path, {
5312
- source: shadow.source,
6200
+ evidence: toCliProjectionCommandEvidence(result),
6201
+ value: {
6202
+ ...value,
6203
+ displayPath: toOpsxDisplayPath(value.path, {
6204
+ source: value.source,
5313
6205
  projectDir: this.projectDir
5314
- })
5315
- }))
6206
+ }),
6207
+ shadows: value.shadows.map((shadow) => ({
6208
+ ...shadow,
6209
+ displayPath: toOpsxDisplayPath(shadow.path, {
6210
+ source: shadow.source,
6211
+ projectDir: this.projectDir
6212
+ })
6213
+ }))
6214
+ }
5316
6215
  };
5317
6216
  }
5318
6217
  async fetchSchemaDetail(name) {
5319
6218
  await touchOpsxProjectDeps(this.projectDir);
5320
6219
  await this.ensureSchemaResolution(name);
5321
- const schemaPath = join$1(this.getSchemaResolution(name).path, "schema.yaml");
6220
+ return this.fetchSchemaDetailAtResolution(name, this.getSchemaResolution(name));
6221
+ }
6222
+ async fetchSchemaDetailAtResolution(name, resolution) {
6223
+ const schemaPath = join$1(resolution.path, "schema.yaml");
5322
6224
  const content = await reactiveReadFile(schemaPath);
5323
6225
  if (!content) throw new Error(`schema.yaml not found at ${schemaPath}`);
5324
6226
  return parseSchemaYamlInline(content, name, schemaPath);
@@ -5334,22 +6236,29 @@ var OpsxKernel = class {
5334
6236
  return reactiveReadFile(join$1(this.getSchemaResolution(name).path, "schema.yaml"));
5335
6237
  }
5336
6238
  async fetchTemplates(schema) {
6239
+ return (await this.fetchTemplatesProjection(schema)).value;
6240
+ }
6241
+ async fetchTemplatesProjection(schema) {
5337
6242
  await touchOpsxProjectDeps(this.projectDir);
5338
- const result = await this.cliExecutor.templates(schema);
5339
- if (!result.success) throw new Error(result.stderr || `openspec templates failed (exit ${result.exitCode ?? "null"})`);
5340
- const templates = parseCliJson(result.stdout, TemplatesSchema, "openspec templates");
5341
- return Object.fromEntries(Object.entries(templates).map(([artifactId, info]) => [artifactId, {
5342
- ...info,
5343
- path: toAbsoluteProjectPath(this.projectDir, info.path),
5344
- displayPath: toOpsxDisplayPath(info.path, {
5345
- source: info.source,
5346
- projectDir: this.projectDir
5347
- })
5348
- }]));
6243
+ const result = await this.cliExecutor.contracts.templates(schema);
6244
+ const templates = requireCommandData("openspec templates", result, TemplatesSchema);
6245
+ return {
6246
+ value: Object.fromEntries(Object.entries(templates).map(([artifactId, info]) => [artifactId, {
6247
+ ...info,
6248
+ path: toAbsoluteProjectPath(this.projectDir, info.path),
6249
+ displayPath: toOpsxDisplayPath(info.path, {
6250
+ source: info.source,
6251
+ projectDir: this.projectDir
6252
+ })
6253
+ }])),
6254
+ evidence: toCliProjectionCommandEvidence(result)
6255
+ };
5349
6256
  }
5350
6257
  async fetchTemplateContents(schema) {
5351
6258
  await this.ensureTemplates(schema);
5352
- const templates = this.getTemplates(schema);
6259
+ return this.readTemplateContents(this.getTemplates(schema));
6260
+ }
6261
+ async readTemplateContents(templates) {
5353
6262
  const entries = await Promise.all(Object.entries(templates).map(async ([artifactId, info]) => {
5354
6263
  return [artifactId, {
5355
6264
  content: await reactiveReadFile(info.path),
@@ -5367,7 +6276,67 @@ var OpsxKernel = class {
5367
6276
  return reactiveReadFile(join$1(this.projectDir, "openspec", "changes", changeId, ".openspec.yaml"));
5368
6277
  }
5369
6278
  async fetchArtifactOutput(changeId, outputPath) {
5370
- return reactiveReadFile(join$1(this.projectDir, "openspec", "changes", changeId, outputPath));
6279
+ const location = requireCanonicalArtifactLocation(changeId, outputPath);
6280
+ return reactiveReadFile(join$1(this.projectDir, "openspec", "changes", location.changeId, location.outputPath));
6281
+ }
6282
+ /** Execute one Status projection with dependencies owned by the caller's reactive generation. */
6283
+ readStatusProjection(changeId, schema) {
6284
+ return this.fetchStatus(changeId, schema);
6285
+ }
6286
+ /** Execute CLI-owned Change enumeration with file dependencies owned by the caller's Work generation. */
6287
+ readChangeListProjection() {
6288
+ return this.fetchChangeListProjection();
6289
+ }
6290
+ /** Execute the current Change Status list without joining the Kernel's retained entity streams. */
6291
+ async readStatusListProjection() {
6292
+ const changeList = await this.fetchChangeListProjection();
6293
+ return {
6294
+ value: await Promise.all(changeList.value.map((changeId) => this.fetchStatus(changeId))),
6295
+ evidence: changeList.evidence
6296
+ };
6297
+ }
6298
+ /** Execute one artifact-instructions projection in the caller-owned Work generation. */
6299
+ readInstructionsProjection(changeId, artifact, schema) {
6300
+ return this.fetchInstructions(changeId, artifact, schema);
6301
+ }
6302
+ /** Execute one Apply-instructions projection in the caller-owned Work generation. */
6303
+ readApplyInstructionsProjection(changeId, schema) {
6304
+ return this.fetchApplyInstructions(changeId, schema);
6305
+ }
6306
+ /** Execute the aggregate Schema bundle while retaining exact resolved-file dependencies. */
6307
+ async readConfigBundleProjection() {
6308
+ const schemasProjection = await this.fetchSchemasProjection();
6309
+ const details = await Promise.all(schemasProjection.value.map(async ({ name }) => {
6310
+ const resolution = await this.fetchSchemaResolutionProjection(name);
6311
+ return {
6312
+ name,
6313
+ detail: await this.fetchSchemaDetailAtResolution(name, resolution.value).catch(() => null),
6314
+ resolution
6315
+ };
6316
+ }));
6317
+ return {
6318
+ value: {
6319
+ schemas: schemasProjection.value,
6320
+ schemaDetails: Object.fromEntries(details.map(({ name, detail }) => [name, detail])),
6321
+ schemaResolutions: Object.fromEntries(details.map(({ name, resolution }) => [name, resolution.value]))
6322
+ },
6323
+ evidence: {
6324
+ schemas: schemasProjection.evidence,
6325
+ schemaResolutions: Object.fromEntries(details.map(({ name, resolution }) => [name, resolution.evidence]))
6326
+ }
6327
+ };
6328
+ }
6329
+ /** Execute one CLI template-index projection. */
6330
+ readTemplatesProjection(schema) {
6331
+ return this.fetchTemplatesProjection(schema);
6332
+ }
6333
+ /** Execute one template-content projection from the CLI-selected template index. */
6334
+ async readTemplateContentsProjection(schema) {
6335
+ const templates = await this.fetchTemplatesProjection(schema);
6336
+ return {
6337
+ value: await this.readTemplateContents(templates.value),
6338
+ evidence: templates.evidence
6339
+ };
5371
6340
  }
5372
6341
  async ensureSchemas() {
5373
6342
  await this.startStreamOnce("global:schemas", this._schemas, () => this.fetchSchemas(), this.controller.signal);
@@ -5375,36 +6344,38 @@ var OpsxKernel = class {
5375
6344
  async ensureChangeIds() {
5376
6345
  await this.startStreamOnce("global:change-ids", this._changeIds, () => this.fetchChangeIds(), this.controller.signal);
5377
6346
  }
5378
- async ensureProjectConfig() {
5379
- await this.startStreamOnce("global:project-config", this._projectConfig, () => this.fetchProjectConfig(), this.controller.signal);
5380
- }
5381
6347
  async ensureStatusList() {
5382
6348
  await this.startStreamOnce("global:status-list", this._statusList, () => this.fetchStatusList(), this.controller.signal);
5383
6349
  }
5384
6350
  async ensureStatus(changeId, schema) {
5385
- const key = `${changeId}:${schema ?? ""}`;
6351
+ const canonicalChangeId = requireCanonicalOpenSpecEntityId(changeId, "changeId");
6352
+ const key = `${canonicalChangeId}:${schema ?? ""}`;
5386
6353
  if (!this._statuses.has(key)) this._statuses.set(key, new ReactiveState(null));
5387
- await this.startStreamOnce(`change:${changeId}:status:${schema ?? ""}`, this._statuses.get(key), () => this.fetchStatus(changeId, schema), this.controller.signal);
6354
+ await this.startStreamOnce(`change:${canonicalChangeId}:status:${schema ?? ""}`, this._statuses.get(key), () => this.fetchStatus(canonicalChangeId, schema), this.controller.signal);
5388
6355
  }
5389
6356
  async ensureInstructions(changeId, artifact, schema) {
5390
- const key = `${changeId}:${artifact}:${schema ?? ""}`;
6357
+ const canonicalChangeId = requireCanonicalOpenSpecEntityId(changeId, "changeId");
6358
+ const key = `${canonicalChangeId}:${artifact}:${schema ?? ""}`;
5391
6359
  if (!this._instructions.has(key)) this._instructions.set(key, new ReactiveState(null));
5392
- await this.startStreamOnce(`change:${changeId}:instructions:${artifact}:${schema ?? ""}`, this._instructions.get(key), () => this.fetchInstructions(changeId, artifact, schema), this.controller.signal);
6360
+ await this.startStreamOnce(`change:${canonicalChangeId}:instructions:${artifact}:${schema ?? ""}`, this._instructions.get(key), () => this.fetchInstructions(canonicalChangeId, artifact, schema), this.controller.signal);
5393
6361
  }
5394
6362
  async ensureApplyInstructions(changeId, schema) {
5395
- const key = `${changeId}:${schema ?? ""}`;
6363
+ const canonicalChangeId = requireCanonicalOpenSpecEntityId(changeId, "changeId");
6364
+ const key = `${canonicalChangeId}:${schema ?? ""}`;
5396
6365
  if (!this._applyInstructions.has(key)) this._applyInstructions.set(key, new ReactiveState(null));
5397
- await this.startStreamOnce(`change:${changeId}:apply:${schema ?? ""}`, this._applyInstructions.get(key), () => this.fetchApplyInstructions(changeId, schema), this.controller.signal);
6366
+ await this.startStreamOnce(`change:${canonicalChangeId}:apply:${schema ?? ""}`, this._applyInstructions.get(key), () => this.fetchApplyInstructions(canonicalChangeId, schema), this.controller.signal);
5398
6367
  }
5399
6368
  async ensureArtifactOutput(changeId, outputPath) {
5400
- const key = `${changeId}:${outputPath}`;
6369
+ const location = requireCanonicalArtifactLocation(changeId, outputPath);
6370
+ const key = `${location.changeId}:${location.outputPath}`;
5401
6371
  if (!this._artifactOutputs.has(key)) this._artifactOutputs.set(key, new ReactiveState(null));
5402
- await this.startStreamOnce(`change:${changeId}:output:${outputPath}`, this._artifactOutputs.get(key), () => this.fetchArtifactOutput(changeId, outputPath), this.controller.signal);
6372
+ await this.startStreamOnce(`change:${location.changeId}:output:${location.outputPath}`, this._artifactOutputs.get(key), () => this.fetchArtifactOutput(location.changeId, location.outputPath), this.controller.signal);
5403
6373
  }
5404
6374
  async ensureGlobArtifactFiles(changeId, outputPath) {
5405
- const key = `${changeId}:${outputPath}`;
6375
+ const location = requireCanonicalArtifactLocation(changeId, outputPath);
6376
+ const key = `${location.changeId}:${location.outputPath}`;
5406
6377
  if (!this._globArtifactFiles.has(key)) this._globArtifactFiles.set(key, new ReactiveState([]));
5407
- await this.startStreamOnce(`change:${changeId}:glob:${outputPath}`, this._globArtifactFiles.get(key), () => readGlobArtifactFiles(this.projectDir, changeId, outputPath), this.controller.signal);
6378
+ await this.startStreamOnce(`change:${location.changeId}:glob:${location.outputPath}`, this._globArtifactFiles.get(key), () => readGlobArtifactFiles(this.projectDir, location.changeId, location.outputPath), this.controller.signal);
5408
6379
  }
5409
6380
  async ensureSchemaResolution(name) {
5410
6381
  if (!this._schemaResolutions.has(name)) this._schemaResolutions.set(name, new ReactiveState(null));
@@ -5433,8 +6404,9 @@ var OpsxKernel = class {
5433
6404
  await this.startStreamOnce(`schema:${key}:template-contents`, this._templateContents.get(key), () => this.fetchTemplateContents(schema), this.controller.signal);
5434
6405
  }
5435
6406
  async ensureChangeMetadata(changeId) {
5436
- if (!this._changeMetadata.has(changeId)) this._changeMetadata.set(changeId, new ReactiveState(null));
5437
- await this.startStreamOnce(`change:${changeId}:metadata`, this._changeMetadata.get(changeId), () => this.fetchChangeMetadata(changeId), this.controller.signal);
6407
+ const canonicalChangeId = requireCanonicalOpenSpecEntityId(changeId, "changeId");
6408
+ if (!this._changeMetadata.has(canonicalChangeId)) this._changeMetadata.set(canonicalChangeId, new ReactiveState(null));
6409
+ await this.startStreamOnce(`change:${canonicalChangeId}:metadata`, this._changeMetadata.get(canonicalChangeId), () => this.fetchChangeMetadata(canonicalChangeId), this.controller.signal);
5438
6410
  }
5439
6411
  combineSignals(a, b) {
5440
6412
  const ctrl = new AbortController();
@@ -5454,12 +6426,23 @@ function parseSchemaYamlInline(content, fallbackName, path) {
5454
6426
 
5455
6427
  //#endregion
5456
6428
  //#region src/pty-protocol.ts
6429
+ /**
6430
+ * Orthogonal intents (updated 2026-07-22 Asia/Shanghai):
6431
+ * 1. Define the type-safe PTY client/server transport protocol.
6432
+ * 2. Preserve explicit launch-project or planning-root cwd identity across create/list/reconnect.
6433
+ * 3. Carry terminal output, control metadata, lifecycle, and failure messages.
6434
+ * 4. Carry opaque planning-root generation evidence through PTY creation and guarded workflow input.
6435
+ *
6436
+ * Original request (2026-07-16): "3.8 Terminal exposes explicit launch-project cwd and planning-root cwd while preserving inherited XDG_DATA_HOME"
6437
+ * Owner-reported defect (2026-07-21): Same-generation Agent terminals are unavailable to Send.
6438
+ */
5457
6439
  const PositiveInt = z.number().int().positive();
5458
6440
  const PtyPlatformSchema = z.enum([
5459
6441
  "windows",
5460
6442
  "macos",
5461
6443
  "common"
5462
6444
  ]);
6445
+ const TerminalCwdTargetSchema = z.enum(["launch-project", "planning-root"]);
5463
6446
  const CloseCallbackUrlSchema = z.union([z.string(), z.record(z.string())]);
5464
6447
  const PtySessionInfoSchema = z.object({
5465
6448
  id: z.string().min(1),
@@ -5470,7 +6453,10 @@ const PtySessionInfoSchema = z.object({
5470
6453
  isExited: z.boolean(),
5471
6454
  exitCode: z.number().int().nullable(),
5472
6455
  closeTip: z.string().optional(),
5473
- closeCallbackUrl: CloseCallbackUrlSchema.optional()
6456
+ closeCallbackUrl: CloseCallbackUrlSchema.optional(),
6457
+ cwdTarget: TerminalCwdTargetSchema,
6458
+ initialCwd: z.string().min(1),
6459
+ rootGeneration: z.string().min(1).nullable()
5474
6460
  });
5475
6461
  const PtyCreateMessageSchema = z.object({
5476
6462
  type: z.literal("create"),
@@ -5479,6 +6465,8 @@ const PtyCreateMessageSchema = z.object({
5479
6465
  rows: PositiveInt.optional(),
5480
6466
  command: z.string().min(1).optional(),
5481
6467
  args: z.array(z.string()).optional(),
6468
+ cwdTarget: TerminalCwdTargetSchema,
6469
+ expectedRootGeneration: z.string().min(1).optional(),
5482
6470
  closeTip: z.string().optional(),
5483
6471
  closeCallbackUrl: CloseCallbackUrlSchema.optional()
5484
6472
  });
@@ -5487,6 +6475,14 @@ const PtyInputMessageSchema = z.object({
5487
6475
  sessionId: z.string().min(1),
5488
6476
  data: z.string()
5489
6477
  });
6478
+ /** A workflow payload whose terminal and current Planning-root generation require Server approval. */
6479
+ const PtyWorkflowInputMessageSchema = z.object({
6480
+ type: z.literal("workflow-input"),
6481
+ requestId: z.string().min(1),
6482
+ sessionId: z.string().min(1),
6483
+ expectedRootGeneration: z.string().min(1),
6484
+ data: z.string()
6485
+ });
5490
6486
  const PtyResizeMessageSchema = z.object({
5491
6487
  type: z.literal("resize"),
5492
6488
  sessionId: z.string().min(1),
@@ -5507,6 +6503,7 @@ const PtyListMessageSchema = z.object({ type: z.literal("list") });
5507
6503
  const PtyClientMessageSchema = z.discriminatedUnion("type", [
5508
6504
  PtyCreateMessageSchema,
5509
6505
  PtyInputMessageSchema,
6506
+ PtyWorkflowInputMessageSchema,
5510
6507
  PtyResizeMessageSchema,
5511
6508
  PtyCloseMessageSchema,
5512
6509
  PtyAttachMessageSchema,
@@ -5516,7 +6513,23 @@ const PtyCreatedResponseSchema = z.object({
5516
6513
  type: z.literal("created"),
5517
6514
  requestId: z.string().min(1),
5518
6515
  sessionId: z.string().min(1),
5519
- platform: PtyPlatformSchema
6516
+ platform: PtyPlatformSchema,
6517
+ cwdTarget: TerminalCwdTargetSchema,
6518
+ initialCwd: z.string().min(1),
6519
+ rootGeneration: z.string().min(1).nullable()
6520
+ });
6521
+ /** Confirms that guarded workflow input reached the Server-owned PTY session. */
6522
+ const PtyWorkflowInputAcceptedResponseSchema = z.object({
6523
+ type: z.literal("workflow-input-accepted"),
6524
+ requestId: z.string().min(1),
6525
+ sessionId: z.string().min(1)
6526
+ });
6527
+ /** Rejects guarded workflow input before PTY write while preserving the Server-owned reason. */
6528
+ const PtyWorkflowInputRejectedResponseSchema = z.object({
6529
+ type: z.literal("workflow-input-rejected"),
6530
+ requestId: z.string().min(1),
6531
+ sessionId: z.string().min(1),
6532
+ message: z.string().min(1)
5520
6533
  });
5521
6534
  const PtyOutputResponseSchema = z.object({
5522
6535
  type: z.literal("output"),
@@ -5585,7 +6598,8 @@ const PtyErrorCodeSchema = z.enum([
5585
6598
  "INVALID_JSON",
5586
6599
  "INVALID_MESSAGE",
5587
6600
  "SESSION_NOT_FOUND",
5588
- "PTY_CREATE_FAILED"
6601
+ "PTY_CREATE_FAILED",
6602
+ "UNAUTHORIZED"
5589
6603
  ]);
5590
6604
  const PtyErrorResponseSchema = z.object({
5591
6605
  type: z.literal("error"),
@@ -5595,6 +6609,8 @@ const PtyErrorResponseSchema = z.object({
5595
6609
  });
5596
6610
  const PtyServerMessageSchema = z.discriminatedUnion("type", [
5597
6611
  PtyCreatedResponseSchema,
6612
+ PtyWorkflowInputAcceptedResponseSchema,
6613
+ PtyWorkflowInputRejectedResponseSchema,
5598
6614
  PtyOutputResponseSchema,
5599
6615
  PtyExitResponseSchema,
5600
6616
  PtyTitleResponseSchema,
@@ -5609,4 +6625,4 @@ const PtyServerMessageSchema = z.discriminatedUnion("type", [
5609
6625
  ]);
5610
6626
 
5611
6627
  //#endregion
5612
- export { AI_TOOLS, ApplyInstructionsContextFilesSchema, ApplyInstructionsSchema, ApplyTaskSchema, ArtifactInstructionsSchema, ArtifactStatusSchema, BUILTIN_SOUND_IDS, BUILTIN_SOUND_OPTIONS, BUILTIN_TERMINAL_SPAWN_COMMANDS, BatchTranslateEventSchema, BatchTranslateInputSchema, BuiltinSoundIdSchema, CODE_EDITOR_THEME_VALUES, CUSTOM_SOUND_ADD_VALUE, ChangeFileSchema, ChangeSchema, ChangeStatusSchema, CliExecutor, CodeEditorThemeSchema, ConfigManager, CustomSoundHashSchema, CustomSoundIdSchema, CustomSoundMetadataFileSchema, CustomSoundMetadataSchema, DASHBOARD_METRIC_KEYS, DEFAULT_BELL_SOUND_ID, DEFAULT_CONFIG, DEFAULT_GIT_DIFF_EAGER_LINE_BUDGET, DEFAULT_GLOBAL_SETTINGS, DEFAULT_NOTIFICATION_SOUND_ID, DEFAULT_TERMINAL_DARK_THEME, DEFAULT_TERMINAL_LIGHT_THEME, DEFAULT_TERMINAL_THEME_MODE, DEFAULT_TRANSLATION_CACHE_ENTRY_LIMIT, DEFAULT_TRANSLATION_ENGINE_ID, DOCUMENT_TRANSLATION_DISPLAY_MODES, DashboardConfigSchema, DeltaOperationType, DeltaSchema, DeltaSpecSchema, DependencyInfoSchema, DocumentTranslationConfigSchema, DocumentTranslationConfigUpdateSchema, DocumentTranslationDisplayModeSchema, DocumentTranslationGlobalSettingsUpdateSchema, FILE_PREVIEW_KINDS, GitConfigSchema, GlobalSettingsManager, HOSTED_SHELL_PROTOCOL_VERSION, LEGACY_SOUND_ID_MAP, LOCAL_MODEL_PROFILE_DEFINITIONS, LocalModelAssetLogSchema, LocalModelAssetPlanSnapshotSchema, LocalModelAssetStateSchema, LocalModelDownloadStatusSchema, LocalModelLifecycleFileStateSchema, LocalModelLifecycleGroupStateSchema, LocalModelProfileLoadStateSchema, LocalModelProfileManifestFileSchema, LocalModelProfileManifestGroupSchema, LocalModelProfileManifestSchema, LocalModelProfileStatusSchema, MAX_TRANSLATION_CACHE_ENTRY_LIMIT, MIN_TRANSLATION_CACHE_ENTRY_LIMIT, ManagedLocalCatalogSourceSchema, MarkdownParser, MarkdownReadingPluginRegistry, NOTIFICATION_SOUND_OPTIONS, NOTIFICATION_SOUND_VALUES, NotificationActionSchema, NotificationGroupKeySchema, NotificationPublishInputSchema, NotificationRecordSchema, NotificationSettingsSchema, NotificationSoundSchema, NotificationSourceSchema, OFFICIAL_APP_BASE_URL, OPENSPECUI_HOOKS_VERSION, OPENSPECUI_RUNTIME_CAPABILITIES, OPEN_SPEC_READING_SECTIONS_PROJECTION_ID, OPEN_SPEC_SPEC_PROJECTION_ID, OPSX_AGENT_INVOCATION_MODE_VALUES, OpenSpecAdapter, OpenSpecUIConfigSchema, OpenSpecUIGlobalSettingsSchema, OpenSpecUIGlobalSettingsUpdateSchema, OpenSpecWatcher, OpsxAgentInvocationModeSchema, OpsxConfigSchema, OpsxKernel, ProjectWatcher, PtyAttachMessageSchema, PtyBellResponseSchema, PtyBufferResponseSchema, PtyClientMessageSchema, PtyCloseMessageSchema, PtyCreateMessageSchema, PtyCreatedResponseSchema, PtyErrorCodeSchema, PtyErrorResponseSchema, PtyExitResponseSchema, PtyInputMessageSchema, PtyListMessageSchema, PtyListResponseSchema, PtyOutputResponseSchema, PtyPlatformSchema, PtyProcessTitleResponseSchema, PtyResizeMessageSchema, PtyServerMessageSchema, PtyTitleResponseSchema, ReactiveContext, ReactiveState, RequirementSchema, SERVICE_TRANSLATION_ENGINE_IDS, SILENT_SOUND_ID, ScenarioStepKeywordSchema, ScenarioStepSchema, SchemaArtifactSchema, SchemaDetailSchema, SchemaInfoSchema, SchemaResolutionSchema, ServiceTranslationEngineIdSchema, SoundConfigIdSchema, SoundIdSchema, SpecSchema, StoreDoctorResultSchema, StoreListResultSchema, TERMINAL_BELL_SOUND_OPTIONS, TERMINAL_BELL_SOUND_VALUES, TERMINAL_COMMAND_FIELD_TYPE_VALUES, TERMINAL_SHELL_QUOTE_STYLE_VALUES, TERMINAL_THEME_MODE_VALUES, TERMINAL_THEME_VALUES, TOOL_WORKFLOW_TO_SKILL_DIR, TRANSLATION_CACHE_POLICY_VERSION, TRANSLATION_ENGINE_IDS, TRANSLATION_ENGINE_MANIFESTS, TRANSLATOR_CONTRACT_VERSION, TaskSchema, TemplatesSchema, TerminalBellSoundSchema, TerminalCommandFieldSchema, TerminalConfigSchema, TerminalControlParser, TerminalInvocationSettingsSchema, TerminalNotificationParser, TerminalRendererEngineSchema, TerminalShellProfileSchema, TerminalShellQuoteStyleSchema, TerminalSpawnCommandSchema, TerminalThemeModeSchema, TerminalThemeSchema, TranslationCacheEntrySchema, TranslationCacheReadInputSchema, TranslationCacheSettingsSchema, TranslationCacheSettingsUpdateSchema, TranslationCacheStatsSchema, TranslationCacheWriteInputSchema, TranslationDownloadFilePlanSchema, TranslationDownloadGroupPlanSchema, TranslationEngineAssetStateSchema, TranslationEngineAssetStatusSchema, TranslationEngineDependencyStateSchema, TranslationEngineDependencyStatusSchema, TranslationEngineGlobalSettingsSchema, TranslationEngineGlobalSettingsUpdateSchema, TranslationEngineIdSchema, TranslationEngineInstallLogEventSchema, TranslationEngineInstallLogStreamSchema, TranslationEngineLifecycleEventSchema, TranslationEngineLifecycleStatusSchema, TranslationEngineProjectSettingsSchema, TranslationEngineProjectSettingsUpdateSchema, TranslationEngineRuntimeStateSchema, TranslationEngineRuntimeStatusSchema, TranslationLocalCt2SettingsSchema, TranslationLocalCt2SettingsUpdateSchema, TranslationLocalLlamaSettingsSchema, TranslationLocalLlamaSettingsUpdateSchema, TranslationLocalSettingsSchema, TranslationLocalSettingsUpdateSchema, TranslationOpenAISettingsSchema, TranslationOpenAISettingsUpdateSchema, VIRTUAL_PROJECT_DIRNAME, Validator, acquireWatcher, annotateOpenSpecFacts, annotateOpenSpecMarkdown, buildBackendHealthPayload, buildCliRunnerCandidates, buildEmbeddedUiLaunchUrl, buildHostedLaunchUrl, buildLocalDownloadPlanFromRepositoryFiles, buildLocalDownloadPlanFromRuntimeProfileFiles, buildMarkdownParentMap, buildOpsxEntityDetail, buildRuntimePackageInstallCommand, builtinOpenSpecReadingPlugin, checkLocalDirectionalModelLanguagePair, classifyStoreCliOutput, clearCache, closeAllProjectWatchers, closeAllWatchers, computeTaskProgress, contextStorage, createCleanCliEnv, createFileChangeObservable, createMarkdownReadingDocument, createMarkdownReadingDocumentFromFacts, createOpenSpecReadingPlugin, createTranslationEngineLifecycleStatus, customHashFromSoundId, detectRuntimePackageManager, getActiveWatcherCount, getAllToolIds, getAllTools, getAvailableToolIds, getAvailableTools, getBuiltinSoundUrl, getCacheSize, getConfiguredTools, getDefaultCliCommand, getDefaultCliCommandString, getDefaultGlobalSettingsPath, getDetectedProjectTools, getManagedLocalTranslationEngineManifest, getMarkdownAnnotation, getMarkdownAnnotationsForFact, getMarkdownFactSpan, getMarkdownHeadingEnd, getMarkdownHeadingFacts, getNotificationGroupKey, getNotificationGroupLabel, getOpenSpecAnnotation, getOpenSpecAnnotationsForFact, getOpenSpecProjectionAnnotation, getOpenSpecReadingSections, getOpsxEntityMetadataPath, getOpsxEntityRootRelativePath, getProjectWatcher, getTerminalCommandDefaultValues, getToolById, getToolInitStates, getTranslationEngineLifecycleMessage, getTranslationEngineManifest, getWatchedProjectDir, getWatcherRuntimeStatus, groupNotifications, inferFileMime, inferFilePreviewKind, inferLocalDirectionalModelLanguagePair, initWatcherPool, isBackendHealthRuntimeMetadata, isBatchTranslationAbort, isDirectionalManagedLocalTranslationEngineId, isGlobPattern, isHostedBackendHealthResponse, isManagedLocalTranslationEngineId, isMarkdownTaskSourcePath, isOpsxGlobPattern, isPreviewableFile, isSupportedEmbeddedUiUrl, isTerminalRendererEngine, isTextLikeFile, isTextLikeMime, isToolConfigured, isTranslationEngineDependencyReady, isTranslationEngineRuntimeReady, isWatcherPoolInitialized, normalizeBatchTranslationError, normalizeEmbeddedUiUrl, normalizeHostedAppBaseUrl, normalizeLegacySoundId, normalizeOpsxEntityPath, openSpecAnnotationRules, opsxGlobToRegex, opsxPathMatchesPattern, parseCliCommand, parseMarkdownFacts, parseMarkdownTasks, parseOpenSpecMarkdownToSpec, parseOpsxEntityMetadata, parseOpsxSchemaDetail, projectAnnotatedOpenSpecToSpec, projectOpenSpecMarkdown, projectTasksFromMarkdownFiles, quoteTerminalShellArg, reactiveExists, reactiveReadDir, reactiveReadFile, reactiveStat, renderTerminalCommandArgs, renderTerminalSpawnCommandLine, resolveHostedAppBaseUrl, resolveRuntimePackageInstallStrategy, resolveTerminalShellDefaults, runControlledTranslationTask, selectLocalDownloadGroup, shouldShowTranslationEngineInstallGate, sniffGlobalCli, sortMarkdownReadingPlugins, soundIdFromCustomHash, subscribeWatcherRuntimeStatus, terminalNotificationEventToPublishInput, toMarkdownFactKind, toOpsxDisplayPath, toPersistedGlobalSettings, toStoreFeatureResult, trimMarkdownSlice };
6628
+ export { ACCESS_GATE_CREDENTIAL_BYTES, AI_TOOLS, ApplyInstructionsContextFilesSchema, ApplyInstructionsProjectionSchema, ApplyInstructionsSchema, ApplyTaskSchema, ArtifactInstructionsSchema, ArtifactStatusSchema, BUILTIN_SOUND_IDS, BUILTIN_SOUND_OPTIONS, BUILTIN_TERMINAL_SPAWN_COMMANDS, BatchTranslateEventSchema, BatchTranslateInputSchema, BuiltinSoundIdSchema, CODE_EDITOR_THEME_VALUES, CUSTOM_SOUND_ADD_VALUE, ChangeFileSchema, ChangeSchema, ChangeStatusSchema, CliApplyInstructionsSchema, CliArchiveSchema, CliArtifactInstructionsSchema, CliChangeListSchema, CliContextSchema, CliDiagnosticFailureSchema, CliDiagnosticSchema, CliDoctorReferenceEntrySchema, CliDoctorSchema, CliExecutor, CliJsonValueSchema, CliProjectionCommandError, CliProjectionCommandEvidenceSchema, CliProjectionFailureSchema, CliProjectionInvalidationCauseSchema, CliProjectionNoticeSchema, CliProjectionStateNameSchema, CliReferenceIndexEntrySchema, CliRootSchema, CliRootSourceSchema, CliShowSpecDocumentSchema, CliShowSpecSchema, CliSpecListSchema, CliStoreCleanupSchema, CliStoreDoctorEntrySchema, CliStoreDoctorSchema, CliStoreListSchema, CliStoreMutationSchema, CliStoreSchema, CliStreamTerminationError, CliValidateSchema, CliWorkflowStatusSchema, CodeEditorThemeSchema, ConfigManager, CustomSoundHashSchema, CustomSoundIdSchema, CustomSoundMetadataFileSchema, CustomSoundMetadataSchema, DASHBOARD_METRIC_KEYS, DEFAULT_BELL_SOUND_ID, DEFAULT_CONFIG, DEFAULT_GIT_DIFF_EAGER_LINE_BUDGET, DEFAULT_GLOBAL_SETTINGS, DEFAULT_NOTIFICATION_SOUND_ID, DEFAULT_TERMINAL_DARK_THEME, DEFAULT_TERMINAL_LIGHT_THEME, DEFAULT_TERMINAL_THEME_MODE, DEFAULT_TRANSLATION_CACHE_ENTRY_LIMIT, DEFAULT_TRANSLATION_ENGINE_ID, DOCUMENT_TRANSLATION_DISPLAY_MODES, DashboardConfigSchema, DashboardGitSnapshotSchema, DashboardOverviewSchema, DashboardSummaryIdentitySchema, DashboardSummaryInvalidationCauseSchema, DashboardSummaryInvalidationSchema, DashboardSummaryProjectionSchema, DashboardSummaryProjectionStateSchema, DashboardTrendsProjectionSchema, DeltaOperationType, DeltaSchema, DeltaSpecSchema, DependencyInfoSchema, DocumentTranslationConfigSchema, DocumentTranslationConfigUpdateSchema, DocumentTranslationDisplayModeSchema, DocumentTranslationGlobalSettingsUpdateSchema, EnvironmentGlobalConfigValueSchema, EnvironmentGlobalFileProjectionDataSchema, EnvironmentGlobalFileProjectionStateSchema, EnvironmentGlobalProjectionDataSchema, EnvironmentGlobalProjectionStateSchema, FILE_PREVIEW_KINDS, GitConfigSchema, GlobalSettingsManager, HOSTED_SHELL_PROTOCOL_VERSION, LEGACY_SOUND_ID_MAP, LOCAL_MODEL_PROFILE_DEFINITIONS, LocalModelAssetLogSchema, LocalModelAssetPlanSnapshotSchema, LocalModelAssetStateSchema, LocalModelDownloadStatusSchema, LocalModelLifecycleFileStateSchema, LocalModelLifecycleGroupStateSchema, LocalModelProfileLoadStateSchema, LocalModelProfileManifestFileSchema, LocalModelProfileManifestGroupSchema, LocalModelProfileManifestSchema, LocalModelProfileStatusSchema, MAX_TRANSLATION_CACHE_ENTRY_LIMIT, MIN_TRANSLATION_CACHE_ENTRY_LIMIT, ManagedLocalCatalogSourceSchema, MarkdownParser, MarkdownReadingPluginRegistry, NOTIFICATION_SOUND_OPTIONS, NOTIFICATION_SOUND_VALUES, NotificationActionSchema, NotificationGroupKeySchema, NotificationPublishInputSchema, NotificationRecordSchema, NotificationSettingsSchema, NotificationSoundSchema, NotificationSourceSchema, OFFICIAL_APP_BASE_URL, OPENSPECUI_DOCUMENT_HOOK_VERSION, OPENSPECUI_RUNTIME_CAPABILITIES, OPENSPECUI_WORKFLOW_HOOK_VERSION, OPEN_SPEC_DATA_DIRECTORY_NAME, OPEN_SPEC_DATA_HOME_OBSERVATION_TARGETS, OPEN_SPEC_READING_SECTIONS_PROJECTION_ID, OPEN_SPEC_SPEC_PROJECTION_ID, OPSX_AGENT_INVOCATION_MODE_VALUES, OPSX_ALL_WORKFLOWS, OPSX_ARTIFACT_INPUT_ACTIONS, OPSX_CHANGE_INPUT_ACTIONS, OPSX_COMMAND_CAPABLE_WORKFLOWS, OPSX_CORE_PROFILE_WORKFLOWS, OPSX_TEXT_INPUT_ACTIONS, OPSX_WORKFLOW_LABELS, OPSX_WORKFLOW_TO_SKILL_DIR, OPSX_WORKFLOW_TO_SKILL_DIR as TOOL_WORKFLOW_TO_SKILL_DIR, OpenSpecAdapter, OpenSpecCliContractExecutor, OpenSpecDataHomeObserver, OpenSpecUIConfigSchema, OpenSpecUIGlobalSettingsSchema, OpenSpecUIGlobalSettingsUpdateSchema, OpenSpecWatcher, OpsxAgentInvocationModeSchema, OpsxCliEvidenceSchema, OpsxConfigBundleSchema, OpsxConfigSchema, OpsxKernel, OpsxStatusEvidenceSchema, OwnedSpecIdentitySchema, PlanningCliProjectionDataSchema, PlanningCliProjectionSelectorSchema, PlanningCliProjectionStateSchema, PlanningConfigJsonValueSchema, PlanningConfigReferenceSchema, ProjectBindingUpdateSchema, ProjectWatcher, PtyAttachMessageSchema, PtyBellResponseSchema, PtyBufferResponseSchema, PtyClientMessageSchema, PtyCloseMessageSchema, PtyCreateMessageSchema, PtyCreatedResponseSchema, PtyErrorCodeSchema, PtyErrorResponseSchema, PtyExitResponseSchema, PtyInputMessageSchema, PtyListMessageSchema, PtyListResponseSchema, PtyOutputResponseSchema, PtyPlatformSchema, PtyProcessTitleResponseSchema, PtyResizeMessageSchema, PtyServerMessageSchema, PtyTitleResponseSchema, RUNTIME_INVALIDATION_FACETS, ReactiveContext, ReactiveObservationEnvironment, ReactiveState, ReferencedSpecIdentitySchema, RequirementSchema, RuntimeInvalidationIndex, RuntimeRootInvalidationRegistry, SERVICE_TRANSLATION_ENGINE_IDS, SILENT_SOUND_ID, ScenarioStepKeywordSchema, ScenarioStepSchema, SchemaArtifactSchema, SchemaDetailSchema, SchemaInfoSchema, SchemaResolutionSchema, ServiceTranslationEngineIdSchema, SoundConfigIdSchema, SoundIdSchema, SpecCatalogSchema, SpecCommandEvidenceSchema, SpecDocumentProjectionSchema, SpecIdentitySchema, SpecSchema, StoreDoctorResultSchema, StoreListResultSchema, StoreMutationLifecycleEventSchema, StoreMutationResultSchema, StoreMutationSchema, StoreMutationStartResponseSchema, TERMINAL_BELL_SOUND_OPTIONS, TERMINAL_BELL_SOUND_VALUES, TERMINAL_COMMAND_FIELD_TYPE_VALUES, TERMINAL_SHELL_QUOTE_STYLE_VALUES, TERMINAL_THEME_MODE_VALUES, TERMINAL_THEME_VALUES, TRANSLATION_CACHE_POLICY_VERSION, TRANSLATION_ENGINE_IDS, TRANSLATION_ENGINE_MANIFESTS, TRANSLATOR_CONTRACT_VERSION, TaskSchema, TemplateContentMapSchema, TemplateContentSchema, TemplatesSchema, TerminalBellSoundSchema, TerminalCommandFieldSchema, TerminalConfigSchema, TerminalControlParser, TerminalCwdTargetSchema, TerminalInvocationSettingsSchema, TerminalNotificationParser, TerminalRendererEngineSchema, TerminalShellProfileSchema, TerminalShellQuoteStyleSchema, TerminalSpawnCommandSchema, TerminalThemeModeSchema, TerminalThemeSchema, TrackedTaskProgressSchema, TranslationCacheEntrySchema, TranslationCacheReadInputSchema, TranslationCacheSettingsSchema, TranslationCacheSettingsUpdateSchema, TranslationCacheStatsSchema, TranslationCacheWriteInputSchema, TranslationDownloadFilePlanSchema, TranslationDownloadGroupPlanSchema, TranslationEngineAssetStateSchema, TranslationEngineAssetStatusSchema, TranslationEngineDependencyStateSchema, TranslationEngineDependencyStatusSchema, TranslationEngineGlobalSettingsSchema, TranslationEngineGlobalSettingsUpdateSchema, TranslationEngineIdSchema, TranslationEngineInstallLogEventSchema, TranslationEngineInstallLogStreamSchema, TranslationEngineLifecycleEventSchema, TranslationEngineLifecycleStatusSchema, TranslationEngineProjectSettingsSchema, TranslationEngineProjectSettingsUpdateSchema, TranslationEngineRuntimeStateSchema, TranslationEngineRuntimeStatusSchema, TranslationLocalCt2SettingsSchema, TranslationLocalCt2SettingsUpdateSchema, TranslationLocalLlamaSettingsSchema, TranslationLocalLlamaSettingsUpdateSchema, TranslationLocalSettingsSchema, TranslationLocalSettingsUpdateSchema, TranslationOpenAISettingsSchema, TranslationOpenAISettingsUpdateSchema, VIRTUAL_PROJECT_DIRNAME, Validator, accessGateFingerprint, acquireWatcher, acquireWatcherRoot, annotateOpenSpecFacts, annotateOpenSpecMarkdown, asEnvUri, buildBackendHealthPayload, buildCliRunnerCandidates, buildEmbeddedUiLaunchUrl, buildHostedLaunchUrl, buildLocalDownloadPlanFromRepositoryFiles, buildLocalDownloadPlanFromRuntimeProfileFiles, buildMarkdownParentMap, buildOpsxEntityDetail, buildRuntimePackageInstallCommand, buildSpecCatalog, builtinOpenSpecReadingPlugin, checkLocalDirectionalModelLanguagePair, classifyStoreCliResult, clearCache, cliProjectionStates, closeAllProjectWatchers, closeAllWatchers, computeEnvUri, constantTimeEqual, contextStorage, createApplyInstructionProgress, createCleanCliEnv, createCliProjectionStateSchema, createDocumentChecklistSummary, createFileChangeObservable, createMarkdownReadingDocument, createMarkdownReadingDocumentFromFacts, createOpenSpecReadingPlugin, createPhysicalReactiveDirectory, createStaticSpecCatalogOwnedProjection, createStaticSpecCatalogReferenceProjection, createStaticSpecCatalogReferenceSource, createToolInitStateProjection, createTrackedTaskProgress, createTranslationEngineLifecycleStatus, customHashFromSoundId, deriveTrackedTaskPhase, detectRuntimePackageManager, generateAccessGateCredential, getActiveWatcherCount, getAllToolIds, getAllTools, getAvailableToolIds, getAvailableTools, getBuiltinSoundUrl, getCacheSize, getConfiguredTools, getDefaultCliCommand, getDefaultCliCommandString, getDefaultGlobalSettingsPath, getDetectedProjectTools, getExternalCodexCommandObservationRoot, getManagedLocalTranslationEngineManifest, getMarkdownAnnotation, getMarkdownAnnotationsForFact, getMarkdownFactSpan, getMarkdownHeadingEnd, getMarkdownHeadingFacts, getNotificationGroupKey, getNotificationGroupLabel, getOpenSpecAnnotation, getOpenSpecAnnotationsForFact, getOpenSpecProjectionAnnotation, getOpenSpecReadingSections, getOpsxEntityMetadataPath, getOpsxEntityRootRelativePath, getProjectWatcher, getRootContextCliSelector, getSpecCatalogEntry, getTerminalCommandDefaultValues, getToolById, getToolInitStates, getTranslationEngineLifecycleMessage, getTranslationEngineManifest, getWatcherRuntimeStatus, groupNotifications, hasCapability, inferFileMime, inferFilePreviewKind, inferLocalDirectionalModelLanguagePair, inspectProjectBinding, isBackendHealthRuntimeMetadata, isBatchTranslationAbort, isDirectionalManagedLocalTranslationEngineId, isGlobPattern, isHostedBackendHealthResponse, isManagedLocalTranslationEngineId, isMarkdownTaskSourcePath, isOpsxCoreWorkflowSelection, isOpsxGlobPattern, isPreviewableFile, isReferencedSpecIdentity, isSupportedEmbeddedUiUrl, isTerminalMutationStatus, isTerminalRendererEngine, isTextLikeFile, isTextLikeMime, isToolConfigured, isTranslationEngineDependencyReady, isTranslationEngineRuntimeReady, isWatcherPoolInitialized, mergeSpecCatalog, normalizeAccessGatePassword, normalizeBatchTranslationError, normalizeEmbeddedUiUrl, normalizeHostedAppBaseUrl, normalizeLegacySoundId, normalizeOpsxEntityPath, openSpecAnnotationRules, opsxGlobToRegex, opsxPathMatchesPattern, parseCliCommand, parseCliCommandResult, parseMarkdownFacts, parseMarkdownTasks, parseOpenSpecMarkdownToSpec, parseOpsxEntityMetadata, parseOpsxSchemaDetail, projectAnnotatedOpenSpecToSpec, projectOpenSpecMarkdown, projectTaskProjectionsFromMarkdownFiles, quoteTerminalShellArg, reactiveExists, reactiveReadDir, reactiveReadFile, reactiveStat, redactSnapshotForPublication, removePhysicalReactivePath, renderTerminalCommandArgs, renderTerminalSpawnCommandLine, requireCanonicalOpenSpecEntityId, requireOpenSpecEntityRelativePath, resolveHostedAppBaseUrl, resolveOpenSpecDataScope, resolveRootContext, resolveRuntimePackageInstallStrategy, resolveTerminalShellDefaults, runControlledTranslationTask, runPhysicalReactivePathMutation, selectLocalDownloadGroup, shouldShowTranslationEngineInstallGate, snapshotHasAbsolutePath, sniffGlobalCli, sortMarkdownReadingPlugins, soundIdFromCustomHash, specIdentityFromRoute, specIdentityKey, specRoutePath, subscribeWatcherRuntimeStatus, terminalNotificationEventToPublishInput, toCliProjectionCommandEvidence, toCliProjectionFailure, toCliProjectionNotice, toMarkdownFactKind, toOpsxDisplayPath, toPersistedGlobalSettings, toStoreFeatureResult, toggleMarkdownTask, trimMarkdownSlice, updateProjectBindingContent, updateReactiveFileCache, writePhysicalReactiveFile };