devworkspace 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (187) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/LICENSE +21 -0
  3. package/README.md +127 -3
  4. package/dist/agents/claude/adapter.d.ts +10 -0
  5. package/dist/agents/claude/adapter.js +97 -0
  6. package/dist/agents/claude/adapter.js.map +1 -0
  7. package/dist/agents/claude/content.d.ts +7 -0
  8. package/dist/agents/claude/content.js +136 -0
  9. package/dist/agents/claude/content.js.map +1 -0
  10. package/dist/agents/types.d.ts +26 -0
  11. package/dist/agents/types.js +2 -0
  12. package/dist/agents/types.js.map +1 -0
  13. package/dist/cli/args.d.ts +194 -0
  14. package/dist/cli/args.js +93 -0
  15. package/dist/cli/args.js.map +1 -0
  16. package/dist/cli/help.d.ts +10 -0
  17. package/dist/cli/help.js +169 -0
  18. package/dist/cli/help.js.map +1 -0
  19. package/dist/cli/main.d.ts +2 -0
  20. package/dist/cli/main.js +19 -0
  21. package/dist/cli/main.js.map +1 -0
  22. package/dist/cli/render.d.ts +39 -0
  23. package/dist/cli/render.js +211 -0
  24. package/dist/cli/render.js.map +1 -0
  25. package/dist/cli/run.d.ts +10 -0
  26. package/dist/cli/run.js +329 -0
  27. package/dist/cli/run.js.map +1 -0
  28. package/dist/config/schemas.d.ts +76 -0
  29. package/dist/config/schemas.js +83 -0
  30. package/dist/config/schemas.js.map +1 -0
  31. package/dist/config/validate.d.ts +22 -0
  32. package/dist/config/validate.js +76 -0
  33. package/dist/config/validate.js.map +1 -0
  34. package/dist/core/migrations.d.ts +15 -0
  35. package/dist/core/migrations.js +34 -0
  36. package/dist/core/migrations.js.map +1 -0
  37. package/dist/core/version.d.ts +2 -0
  38. package/dist/core/version.js +12 -0
  39. package/dist/core/version.js.map +1 -0
  40. package/dist/core/versioned.d.ts +13 -0
  41. package/dist/core/versioned.js +16 -0
  42. package/dist/core/versioned.js.map +1 -0
  43. package/dist/core/workspace.d.ts +34 -0
  44. package/dist/core/workspace.js +92 -0
  45. package/dist/core/workspace.js.map +1 -0
  46. package/dist/decisions/decision.d.ts +31 -0
  47. package/dist/decisions/decision.js +123 -0
  48. package/dist/decisions/decision.js.map +1 -0
  49. package/dist/discovery/commands.d.ts +12 -0
  50. package/dist/discovery/commands.js +92 -0
  51. package/dist/discovery/commands.js.map +1 -0
  52. package/dist/discovery/detect.d.ts +72 -0
  53. package/dist/discovery/detect.js +421 -0
  54. package/dist/discovery/detect.js.map +1 -0
  55. package/dist/discovery/ignore.d.ts +6 -0
  56. package/dist/discovery/ignore.js +76 -0
  57. package/dist/discovery/ignore.js.map +1 -0
  58. package/dist/discovery/scan.d.ts +24 -0
  59. package/dist/discovery/scan.js +132 -0
  60. package/dist/discovery/scan.js.map +1 -0
  61. package/dist/errors/index.d.ts +25 -0
  62. package/dist/errors/index.js +39 -0
  63. package/dist/errors/index.js.map +1 -0
  64. package/dist/git/git.d.ts +27 -0
  65. package/dist/git/git.js +97 -0
  66. package/dist/git/git.js.map +1 -0
  67. package/dist/index.d.ts +26 -0
  68. package/dist/index.js +21 -0
  69. package/dist/index.js.map +1 -0
  70. package/dist/integrations/agent-skills.d.ts +10 -0
  71. package/dist/integrations/agent-skills.js +45 -0
  72. package/dist/integrations/agent-skills.js.map +1 -0
  73. package/dist/integrations/graphify.d.ts +7 -0
  74. package/dist/integrations/graphify.js +54 -0
  75. package/dist/integrations/graphify.js.map +1 -0
  76. package/dist/integrations/index.d.ts +6 -0
  77. package/dist/integrations/index.js +30 -0
  78. package/dist/integrations/index.js.map +1 -0
  79. package/dist/integrations/omniroute.d.ts +7 -0
  80. package/dist/integrations/omniroute.js +24 -0
  81. package/dist/integrations/omniroute.js.map +1 -0
  82. package/dist/integrations/ponytail.d.ts +10 -0
  83. package/dist/integrations/ponytail.js +58 -0
  84. package/dist/integrations/ponytail.js.map +1 -0
  85. package/dist/integrations/probe.d.ts +6 -0
  86. package/dist/integrations/probe.js +41 -0
  87. package/dist/integrations/probe.js.map +1 -0
  88. package/dist/integrations/types.d.ts +25 -0
  89. package/dist/integrations/types.js +2 -0
  90. package/dist/integrations/types.js.map +1 -0
  91. package/dist/knowledge/facts.d.ts +43 -0
  92. package/dist/knowledge/facts.js +195 -0
  93. package/dist/knowledge/facts.js.map +1 -0
  94. package/dist/portability/bundle.d.ts +22 -0
  95. package/dist/portability/bundle.js +102 -0
  96. package/dist/portability/bundle.js.map +1 -0
  97. package/dist/prompts/onboarding.d.ts +19 -0
  98. package/dist/prompts/onboarding.js +53 -0
  99. package/dist/prompts/onboarding.js.map +1 -0
  100. package/dist/prompts/refresh.d.ts +15 -0
  101. package/dist/prompts/refresh.js +33 -0
  102. package/dist/prompts/refresh.js.map +1 -0
  103. package/dist/prompts/requirements.d.ts +9 -0
  104. package/dist/prompts/requirements.js +34 -0
  105. package/dist/prompts/requirements.js.map +1 -0
  106. package/dist/security/files.d.ts +5 -0
  107. package/dist/security/files.js +44 -0
  108. package/dist/security/files.js.map +1 -0
  109. package/dist/security/redact.d.ts +13 -0
  110. package/dist/security/redact.js +43 -0
  111. package/dist/security/redact.js.map +1 -0
  112. package/dist/services/agent.d.ts +11 -0
  113. package/dist/services/agent.js +42 -0
  114. package/dist/services/agent.js.map +1 -0
  115. package/dist/services/analysis.d.ts +45 -0
  116. package/dist/services/analysis.js +151 -0
  117. package/dist/services/analysis.js.map +1 -0
  118. package/dist/services/common.d.ts +18 -0
  119. package/dist/services/common.js +18 -0
  120. package/dist/services/common.js.map +1 -0
  121. package/dist/services/doctor.d.ts +27 -0
  122. package/dist/services/doctor.js +223 -0
  123. package/dist/services/doctor.js.map +1 -0
  124. package/dist/services/init.d.ts +18 -0
  125. package/dist/services/init.js +135 -0
  126. package/dist/services/init.js.map +1 -0
  127. package/dist/services/knowledge.d.ts +40 -0
  128. package/dist/services/knowledge.js +72 -0
  129. package/dist/services/knowledge.js.map +1 -0
  130. package/dist/services/migrate.d.ts +16 -0
  131. package/dist/services/migrate.js +54 -0
  132. package/dist/services/migrate.js.map +1 -0
  133. package/dist/services/portability.d.ts +27 -0
  134. package/dist/services/portability.js +155 -0
  135. package/dist/services/portability.js.map +1 -0
  136. package/dist/services/records.d.ts +8 -0
  137. package/dist/services/records.js +26 -0
  138. package/dist/services/records.js.map +1 -0
  139. package/dist/services/refresh.d.ts +22 -0
  140. package/dist/services/refresh.js +64 -0
  141. package/dist/services/refresh.js.map +1 -0
  142. package/dist/services/status.d.ts +57 -0
  143. package/dist/services/status.js +111 -0
  144. package/dist/services/status.js.map +1 -0
  145. package/dist/services/tasks.d.ts +44 -0
  146. package/dist/services/tasks.js +136 -0
  147. package/dist/services/tasks.js.map +1 -0
  148. package/dist/sessions/session.d.ts +19 -0
  149. package/dist/sessions/session.js +53 -0
  150. package/dist/sessions/session.js.map +1 -0
  151. package/dist/tasks/routing.d.ts +27 -0
  152. package/dist/tasks/routing.js +206 -0
  153. package/dist/tasks/routing.js.map +1 -0
  154. package/dist/tasks/task.d.ts +90 -0
  155. package/dist/tasks/task.js +275 -0
  156. package/dist/tasks/task.js.map +1 -0
  157. package/dist/utils/cleanup.d.ts +3 -0
  158. package/dist/utils/cleanup.js +18 -0
  159. package/dist/utils/cleanup.js.map +1 -0
  160. package/dist/utils/fs.d.ts +17 -0
  161. package/dist/utils/fs.js +164 -0
  162. package/dist/utils/fs.js.map +1 -0
  163. package/dist/utils/lock.d.ts +6 -0
  164. package/dist/utils/lock.js +77 -0
  165. package/dist/utils/lock.js.map +1 -0
  166. package/dist/utils/logger.d.ts +11 -0
  167. package/dist/utils/logger.js +18 -0
  168. package/dist/utils/logger.js.map +1 -0
  169. package/dist/utils/paths.d.ts +18 -0
  170. package/dist/utils/paths.js +95 -0
  171. package/dist/utils/paths.js.map +1 -0
  172. package/dist/utils/proc.d.ts +22 -0
  173. package/dist/utils/proc.js +74 -0
  174. package/dist/utils/proc.js.map +1 -0
  175. package/docs/architecture.md +53 -0
  176. package/docs/benchmarking.md +33 -0
  177. package/docs/claude.md +46 -0
  178. package/docs/contributing.md +64 -0
  179. package/docs/getting-started.md +68 -0
  180. package/docs/integrations.md +51 -0
  181. package/docs/knowledge.md +66 -0
  182. package/docs/portability.md +40 -0
  183. package/docs/routing.md +51 -0
  184. package/docs/security.md +44 -0
  185. package/docs/testing.md +58 -0
  186. package/docs/troubleshooting.md +26 -0
  187. package/package.json +68 -4
@@ -0,0 +1,24 @@
1
+ /**
2
+ * OmniRoute (model routing) is OPTIONAL and outside the V1 execution path.
3
+ * This adapter only reports whether an `omniroute` executable exists so a future provider
4
+ * boundary has somewhere to plug in. Nothing in DevWorkspace requires it, API keys, or model configuration.
5
+ */
6
+ export const omniroute = {
7
+ id: 'omniroute',
8
+ name: 'OmniRoute',
9
+ optional: true,
10
+ usage: 'Not used by DevWorkspace V1.',
11
+ async detect(ctx) {
12
+ const exe = await ctx.which('omniroute');
13
+ return {
14
+ id: 'omniroute',
15
+ name: 'OmniRoute',
16
+ optional: true,
17
+ state: exe ? 'PARTIAL' : 'NOT_DETECTED',
18
+ details: exe ? ['executable found; not used by V1'] : ['optional; not required'],
19
+ remediation: [],
20
+ docsUrl: 'https://github.com/diegosouzapw/OmniRoute',
21
+ };
22
+ },
23
+ };
24
+ //# sourceMappingURL=omniroute.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"omniroute.js","sourceRoot":"","sources":["../../src/integrations/omniroute.ts"],"names":[],"mappings":"AAEA;;;;GAIG;AACH,MAAM,CAAC,MAAM,SAAS,GAAgB;IACpC,EAAE,EAAE,WAAW;IACf,IAAI,EAAE,WAAW;IACjB,QAAQ,EAAE,IAAI;IACd,KAAK,EAAE,8BAA8B;IACrC,KAAK,CAAC,MAAM,CAAC,GAAG;QACd,MAAM,GAAG,GAAG,MAAM,GAAG,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC;QACzC,OAAO;YACL,EAAE,EAAE,WAAW;YACf,IAAI,EAAE,WAAW;YACjB,QAAQ,EAAE,IAAI;YACd,KAAK,EAAE,GAAG,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,cAAc;YACvC,OAAO,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,kCAAkC,CAAC,CAAC,CAAC,CAAC,CAAC,wBAAwB,CAAC;YAChF,WAAW,EAAE,EAAE;YACf,OAAO,EAAE,2CAA2C;SACrD,CAAC;IACJ,CAAC;CACF,CAAC","sourcesContent":["import type { Integration, IntegrationStatus } from './types.js';\n\n/**\n * OmniRoute (model routing) is OPTIONAL and outside the V1 execution path.\n * This adapter only reports whether an `omniroute` executable exists so a future provider\n * boundary has somewhere to plug in. Nothing in DevWorkspace requires it, API keys, or model configuration.\n */\nexport const omniroute: Integration = {\n id: 'omniroute',\n name: 'OmniRoute',\n optional: true,\n usage: 'Not used by DevWorkspace V1.',\n async detect(ctx): Promise<IntegrationStatus> {\n const exe = await ctx.which('omniroute');\n return {\n id: 'omniroute',\n name: 'OmniRoute',\n optional: true,\n state: exe ? 'PARTIAL' : 'NOT_DETECTED',\n details: exe ? ['executable found; not used by V1'] : ['optional; not required'],\n remediation: [],\n docsUrl: 'https://github.com/diegosouzapw/OmniRoute',\n };\n },\n};\n"]}
@@ -0,0 +1,10 @@
1
+ import type { Integration } from './types.js';
2
+ /**
3
+ * Ponytail is a code-quality/simplification skill. Documented install for Claude Code:
4
+ * /plugin marketplace add DietrichGebert/ponytail
5
+ * /plugin install ponytail@ponytail
6
+ * Commands: /ponytail [lite|full|ultra|off], /ponytail-review, /ponytail-audit, /ponytail-debt.
7
+ * Other agents: copy AGENTS.md or install skills/ponytail/SKILL.md. Optional config: ~/.config/ponytail/config.json.
8
+ * Ponytail assists quality review; it is never a substitute for tests.
9
+ */
10
+ export declare const ponytail: Integration;
@@ -0,0 +1,58 @@
1
+ import { promises as fs } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { claudeConfigMentions, skillDirExists } from './probe.js';
4
+ import { pathExists } from '../utils/fs.js';
5
+ const DOCS = 'https://github.com/DietrichGebert/ponytail';
6
+ /**
7
+ * Ponytail is a code-quality/simplification skill. Documented install for Claude Code:
8
+ * /plugin marketplace add DietrichGebert/ponytail
9
+ * /plugin install ponytail@ponytail
10
+ * Commands: /ponytail [lite|full|ultra|off], /ponytail-review, /ponytail-audit, /ponytail-debt.
11
+ * Other agents: copy AGENTS.md or install skills/ponytail/SKILL.md. Optional config: ~/.config/ponytail/config.json.
12
+ * Ponytail assists quality review; it is never a substitute for tests.
13
+ */
14
+ export const ponytail = {
15
+ id: 'ponytail',
16
+ name: 'Ponytail',
17
+ optional: false,
18
+ usage: 'Use /ponytail-review on the diff of meaningful changes (features, refactors, security work) to catch unnecessary complexity and debt; ' +
19
+ 'use /ponytail-audit only when the user asks for a repository-wide quality pass. Quality review never replaces running tests.',
20
+ async detect(ctx) {
21
+ const details = [];
22
+ const plugin = await claudeConfigMentions(ctx, 'ponytail');
23
+ const skill = await skillDirExists(ctx, 'ponytail');
24
+ if (plugin)
25
+ details.push(`referenced in Claude Code config (${plugin})`);
26
+ if (skill)
27
+ details.push(`skill installed (${skill})`);
28
+ let agentsMention = false;
29
+ try {
30
+ agentsMention = (await fs.readFile(path.join(ctx.root, 'AGENTS.md'), 'utf8')).toLowerCase().includes('ponytail');
31
+ }
32
+ catch {
33
+ /* no AGENTS.md */
34
+ }
35
+ if (agentsMention)
36
+ details.push('AGENTS.md references Ponytail');
37
+ const userCfg = path.join(ctx.home, '.config', 'ponytail', 'config.json');
38
+ if (await pathExists(userCfg))
39
+ details.push('user config present (~/.config/ponytail/config.json)');
40
+ const found = plugin || skill || agentsMention;
41
+ return {
42
+ id: 'ponytail',
43
+ name: 'Ponytail',
44
+ optional: false,
45
+ state: found ? 'READY' : 'UNVERIFIED',
46
+ details: found ? details : ['not found in Claude Code settings or project skills; plugin state is held by Claude Code so this check is best-effort'],
47
+ remediation: found
48
+ ? []
49
+ : [
50
+ 'In Claude Code run: /plugin marketplace add DietrichGebert/ponytail',
51
+ 'Then: /plugin install ponytail@ponytail',
52
+ 'Other agents: copy Ponytail\'s AGENTS.md into the project (see its INSTALL.md).',
53
+ ],
54
+ docsUrl: DOCS,
55
+ };
56
+ },
57
+ };
58
+ //# sourceMappingURL=ponytail.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ponytail.js","sourceRoot":"","sources":["../../src/integrations/ponytail.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,IAAI,EAAE,EAAE,MAAM,SAAS,CAAC;AACzC,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B,OAAO,EAAE,oBAAoB,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAClE,OAAO,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAE5C,MAAM,IAAI,GAAG,4CAA4C,CAAC;AAE1D;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAgB;IACnC,EAAE,EAAE,UAAU;IACd,IAAI,EAAE,UAAU;IAChB,QAAQ,EAAE,KAAK;IACf,KAAK,EACH,wIAAwI;QACxI,8HAA8H;IAChI,KAAK,CAAC,MAAM,CAAC,GAAG;QACd,MAAM,OAAO,GAAa,EAAE,CAAC;QAC7B,MAAM,MAAM,GAAG,MAAM,oBAAoB,CAAC,GAAG,EAAE,UAAU,CAAC,CAAC;QAC3D,MAAM,KAAK,GAAG,MAAM,cAAc,CAAC,GAAG,EAAE,UAAU,CAAC,CAAC;QACpD,IAAI,MAAM;YAAE,OAAO,CAAC,IAAI,CAAC,qCAAqC,MAAM,GAAG,CAAC,CAAC;QACzE,IAAI,KAAK;YAAE,OAAO,CAAC,IAAI,CAAC,oBAAoB,KAAK,GAAG,CAAC,CAAC;QACtD,IAAI,aAAa,GAAG,KAAK,CAAC;QAC1B,IAAI,CAAC;YACH,aAAa,GAAG,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,WAAW,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC;QACnH,CAAC;QAAC,MAAM,CAAC;YACP,kBAAkB;QACpB,CAAC;QACD,IAAI,aAAa;YAAE,OAAO,CAAC,IAAI,CAAC,+BAA+B,CAAC,CAAC;QACjE,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,SAAS,EAAE,UAAU,EAAE,aAAa,CAAC,CAAC;QAC1E,IAAI,MAAM,UAAU,CAAC,OAAO,CAAC;YAAE,OAAO,CAAC,IAAI,CAAC,sDAAsD,CAAC,CAAC;QAEpG,MAAM,KAAK,GAAG,MAAM,IAAI,KAAK,IAAI,aAAa,CAAC;QAC/C,OAAO;YACL,EAAE,EAAE,UAAU;YACd,IAAI,EAAE,UAAU;YAChB,QAAQ,EAAE,KAAK;YACf,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,YAAY;YACrC,OAAO,EAAE,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,uHAAuH,CAAC;YACpJ,WAAW,EAAE,KAAK;gBAChB,CAAC,CAAC,EAAE;gBACJ,CAAC,CAAC;oBACE,qEAAqE;oBACrE,yCAAyC;oBACzC,iFAAiF;iBAClF;YACL,OAAO,EAAE,IAAI;SACd,CAAC;IACJ,CAAC;CACF,CAAC","sourcesContent":["import { promises as fs } from 'node:fs';\nimport path from 'node:path';\nimport type { Integration, IntegrationStatus } from './types.js';\nimport { claudeConfigMentions, skillDirExists } from './probe.js';\nimport { pathExists } from '../utils/fs.js';\n\nconst DOCS = 'https://github.com/DietrichGebert/ponytail';\n\n/**\n * Ponytail is a code-quality/simplification skill. Documented install for Claude Code:\n * /plugin marketplace add DietrichGebert/ponytail\n * /plugin install ponytail@ponytail\n * Commands: /ponytail [lite|full|ultra|off], /ponytail-review, /ponytail-audit, /ponytail-debt.\n * Other agents: copy AGENTS.md or install skills/ponytail/SKILL.md. Optional config: ~/.config/ponytail/config.json.\n * Ponytail assists quality review; it is never a substitute for tests.\n */\nexport const ponytail: Integration = {\n id: 'ponytail',\n name: 'Ponytail',\n optional: false,\n usage:\n 'Use /ponytail-review on the diff of meaningful changes (features, refactors, security work) to catch unnecessary complexity and debt; ' +\n 'use /ponytail-audit only when the user asks for a repository-wide quality pass. Quality review never replaces running tests.',\n async detect(ctx): Promise<IntegrationStatus> {\n const details: string[] = [];\n const plugin = await claudeConfigMentions(ctx, 'ponytail');\n const skill = await skillDirExists(ctx, 'ponytail');\n if (plugin) details.push(`referenced in Claude Code config (${plugin})`);\n if (skill) details.push(`skill installed (${skill})`);\n let agentsMention = false;\n try {\n agentsMention = (await fs.readFile(path.join(ctx.root, 'AGENTS.md'), 'utf8')).toLowerCase().includes('ponytail');\n } catch {\n /* no AGENTS.md */\n }\n if (agentsMention) details.push('AGENTS.md references Ponytail');\n const userCfg = path.join(ctx.home, '.config', 'ponytail', 'config.json');\n if (await pathExists(userCfg)) details.push('user config present (~/.config/ponytail/config.json)');\n\n const found = plugin || skill || agentsMention;\n return {\n id: 'ponytail',\n name: 'Ponytail',\n optional: false,\n state: found ? 'READY' : 'UNVERIFIED',\n details: found ? details : ['not found in Claude Code settings or project skills; plugin state is held by Claude Code so this check is best-effort'],\n remediation: found\n ? []\n : [\n 'In Claude Code run: /plugin marketplace add DietrichGebert/ponytail',\n 'Then: /plugin install ponytail@ponytail',\n 'Other agents: copy Ponytail\\'s AGENTS.md into the project (see its INSTALL.md).',\n ],\n docsUrl: DOCS,\n };\n },\n};\n"]}
@@ -0,0 +1,6 @@
1
+ import type { IntegrationContext } from './types.js';
2
+ /** Candidate Claude Code settings/plugin registries, project first then user. Read-only and best effort. */
3
+ export declare function claudeConfigFiles(ctx: IntegrationContext): string[];
4
+ /** True if any Claude Code config file mentions the needle (case-insensitive). Plugin install state belongs to Claude Code, so this can only suggest, never prove. */
5
+ export declare function claudeConfigMentions(ctx: IntegrationContext, needle: string): Promise<string | null>;
6
+ export declare function skillDirExists(ctx: IntegrationContext, name: string): Promise<string | null>;
@@ -0,0 +1,41 @@
1
+ import { promises as fs } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { pathExists } from '../utils/fs.js';
4
+ /** Candidate Claude Code settings/plugin registries, project first then user. Read-only and best effort. */
5
+ export function claudeConfigFiles(ctx) {
6
+ return [
7
+ path.join(ctx.root, '.claude', 'settings.json'),
8
+ path.join(ctx.root, '.claude', 'settings.local.json'),
9
+ path.join(ctx.home, '.claude', 'settings.json'),
10
+ path.join(ctx.home, '.claude', 'plugins', 'installed_plugins.json'),
11
+ path.join(ctx.home, '.claude', 'plugins', 'known_marketplaces.json'),
12
+ ];
13
+ }
14
+ /** True if any Claude Code config file mentions the needle (case-insensitive). Plugin install state belongs to Claude Code, so this can only suggest, never prove. */
15
+ export async function claudeConfigMentions(ctx, needle) {
16
+ const n = needle.toLowerCase();
17
+ for (const file of claudeConfigFiles(ctx)) {
18
+ try {
19
+ const st = await fs.stat(file);
20
+ if (!st.isFile() || st.size > 2 * 1024 * 1024)
21
+ continue;
22
+ if ((await fs.readFile(file, 'utf8')).toLowerCase().includes(n)) {
23
+ return file.startsWith(ctx.root) ? path.relative(ctx.root, file).replace(/\\/g, '/') : '~/' + path.relative(ctx.home, file).replace(/\\/g, '/');
24
+ }
25
+ }
26
+ catch {
27
+ /* missing or unreadable */
28
+ }
29
+ }
30
+ return null;
31
+ }
32
+ export async function skillDirExists(ctx, name) {
33
+ const project = path.join(ctx.root, '.claude', 'skills', name);
34
+ if (await pathExists(project))
35
+ return `.claude/skills/${name}`;
36
+ const user = path.join(ctx.home, '.claude', 'skills', name);
37
+ if (await pathExists(user))
38
+ return `~/.claude/skills/${name}`;
39
+ return null;
40
+ }
41
+ //# sourceMappingURL=probe.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"probe.js","sourceRoot":"","sources":["../../src/integrations/probe.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,IAAI,EAAE,EAAE,MAAM,SAAS,CAAC;AACzC,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAG5C,4GAA4G;AAC5G,MAAM,UAAU,iBAAiB,CAAC,GAAuB;IACvD,OAAO;QACL,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,SAAS,EAAE,eAAe,CAAC;QAC/C,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,SAAS,EAAE,qBAAqB,CAAC;QACrD,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,SAAS,EAAE,eAAe,CAAC;QAC/C,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,SAAS,EAAE,SAAS,EAAE,wBAAwB,CAAC;QACnE,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,SAAS,EAAE,SAAS,EAAE,yBAAyB,CAAC;KACrE,CAAC;AACJ,CAAC;AAED,sKAAsK;AACtK,MAAM,CAAC,KAAK,UAAU,oBAAoB,CAAC,GAAuB,EAAE,MAAc;IAChF,MAAM,CAAC,GAAG,MAAM,CAAC,WAAW,EAAE,CAAC;IAC/B,KAAK,MAAM,IAAI,IAAI,iBAAiB,CAAC,GAAG,CAAC,EAAE,CAAC;QAC1C,IAAI,CAAC;YACH,MAAM,EAAE,GAAG,MAAM,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YAC/B,IAAI,CAAC,EAAE,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,IAAI,GAAG,CAAC,GAAG,IAAI,GAAG,IAAI;gBAAE,SAAS;YACxD,IAAI,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC;gBAChE,OAAO,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;YAClJ,CAAC;QACH,CAAC;QAAC,MAAM,CAAC;YACP,2BAA2B;QAC7B,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,GAAuB,EAAE,IAAY;IACxE,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,IAAI,CAAC,CAAC;IAC/D,IAAI,MAAM,UAAU,CAAC,OAAO,CAAC;QAAE,OAAO,kBAAkB,IAAI,EAAE,CAAC;IAC/D,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,IAAI,CAAC,CAAC;IAC5D,IAAI,MAAM,UAAU,CAAC,IAAI,CAAC;QAAE,OAAO,oBAAoB,IAAI,EAAE,CAAC;IAC9D,OAAO,IAAI,CAAC;AACd,CAAC","sourcesContent":["import { promises as fs } from 'node:fs';\nimport path from 'node:path';\nimport { pathExists } from '../utils/fs.js';\nimport type { IntegrationContext } from './types.js';\n\n/** Candidate Claude Code settings/plugin registries, project first then user. Read-only and best effort. */\nexport function claudeConfigFiles(ctx: IntegrationContext): string[] {\n return [\n path.join(ctx.root, '.claude', 'settings.json'),\n path.join(ctx.root, '.claude', 'settings.local.json'),\n path.join(ctx.home, '.claude', 'settings.json'),\n path.join(ctx.home, '.claude', 'plugins', 'installed_plugins.json'),\n path.join(ctx.home, '.claude', 'plugins', 'known_marketplaces.json'),\n ];\n}\n\n/** True if any Claude Code config file mentions the needle (case-insensitive). Plugin install state belongs to Claude Code, so this can only suggest, never prove. */\nexport async function claudeConfigMentions(ctx: IntegrationContext, needle: string): Promise<string | null> {\n const n = needle.toLowerCase();\n for (const file of claudeConfigFiles(ctx)) {\n try {\n const st = await fs.stat(file);\n if (!st.isFile() || st.size > 2 * 1024 * 1024) continue;\n if ((await fs.readFile(file, 'utf8')).toLowerCase().includes(n)) {\n return file.startsWith(ctx.root) ? path.relative(ctx.root, file).replace(/\\\\/g, '/') : '~/' + path.relative(ctx.home, file).replace(/\\\\/g, '/');\n }\n } catch {\n /* missing or unreadable */\n }\n }\n return null;\n}\n\nexport async function skillDirExists(ctx: IntegrationContext, name: string): Promise<string | null> {\n const project = path.join(ctx.root, '.claude', 'skills', name);\n if (await pathExists(project)) return `.claude/skills/${name}`;\n const user = path.join(ctx.home, '.claude', 'skills', name);\n if (await pathExists(user)) return `~/.claude/skills/${name}`;\n return null;\n}\n"]}
@@ -0,0 +1,25 @@
1
+ export type IntegrationState = 'READY' | 'PARTIAL' | 'NOT_DETECTED' | 'UNVERIFIED' | 'ERROR';
2
+ export interface IntegrationStatus {
3
+ id: string;
4
+ name: string;
5
+ optional: boolean;
6
+ state: IntegrationState;
7
+ details: string[];
8
+ remediation: string[];
9
+ docsUrl: string;
10
+ }
11
+ export interface IntegrationContext {
12
+ root: string;
13
+ home: string;
14
+ env: NodeJS.ProcessEnv;
15
+ which(name: string): Promise<string | null>;
16
+ }
17
+ /** Adapter boundary: core code only ever sees IntegrationStatus, never tool internals. */
18
+ export interface Integration {
19
+ id: string;
20
+ name: string;
21
+ optional: boolean;
22
+ /** Short text embedded in generated agent instructions describing how the agent should use the tool. */
23
+ usage: string;
24
+ detect(ctx: IntegrationContext): Promise<IntegrationStatus>;
25
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/integrations/types.ts"],"names":[],"mappings":"","sourcesContent":["export type IntegrationState = 'READY' | 'PARTIAL' | 'NOT_DETECTED' | 'UNVERIFIED' | 'ERROR';\n\nexport interface IntegrationStatus {\n id: string;\n name: string;\n optional: boolean;\n state: IntegrationState;\n details: string[];\n remediation: string[];\n docsUrl: string;\n}\n\nexport interface IntegrationContext {\n root: string;\n home: string;\n env: NodeJS.ProcessEnv;\n which(name: string): Promise<string | null>;\n}\n\n/** Adapter boundary: core code only ever sees IntegrationStatus, never tool internals. */\nexport interface Integration {\n id: string;\n name: string;\n optional: boolean;\n /** Short text embedded in generated agent instructions describing how the agent should use the tool. */\n usage: string;\n detect(ctx: IntegrationContext): Promise<IntegrationStatus>;\n}\n"]}
@@ -0,0 +1,43 @@
1
+ import { type Confidence, type Fact, type FactsFile, type FactStatus, type SourceType } from '../config/schemas.js';
2
+ import type { Ws } from '../core/workspace.js';
3
+ export interface FactInput {
4
+ key: string;
5
+ value: string;
6
+ source: string;
7
+ sourceType: SourceType;
8
+ sources?: string[];
9
+ confidence: Confidence;
10
+ status?: FactStatus;
11
+ }
12
+ export declare const MISSING_HASH = "MISSING";
13
+ export declare function emptyFacts(): FactsFile;
14
+ export declare function loadFacts(ws: Ws): Promise<FactsFile>;
15
+ export declare function saveFacts(ws: Ws, file: FactsFile): Promise<void>;
16
+ /**
17
+ * Authority rank. Mirrors the hierarchy: user decisions > current repository evidence >
18
+ * verified facts > AI-derived findings > stale/unknown. Lower-ranked input never silently replaces a higher-ranked fact.
19
+ */
20
+ export declare function authorityRank(f: Pick<Fact, 'status' | 'sourceType'>): number;
21
+ export declare function factId(key: string): string;
22
+ export declare function hashSources(root: string, sources: string[], cache?: Map<string, string>): Promise<Record<string, string>>;
23
+ export declare function buildFact(root: string, input: FactInput, now: string, existing?: Fact): Promise<Fact>;
24
+ export type UpsertAction = 'created' | 'updated' | 'unchanged';
25
+ export declare function upsertFact(file: FactsFile, fact: Fact): UpsertAction;
26
+ export interface StalenessReport {
27
+ checked: number;
28
+ newlyStale: {
29
+ key: string;
30
+ reason: string;
31
+ }[];
32
+ alreadyStale: string[];
33
+ /** User decisions whose sources changed. They are never auto-staled; a human should review them. */
34
+ needsReview: {
35
+ key: string;
36
+ reason: string;
37
+ }[];
38
+ }
39
+ /** Compare stored source hashes with the repository now. Mutates facts that became stale. */
40
+ export declare function applyStaleness(root: string, file: FactsFile, now: string): Promise<StalenessReport>;
41
+ /** Re-hash a fact's sources and mark it verified (called after the agent re-checks it against the repository). */
42
+ export declare function verifyFact(root: string, file: FactsFile, key: string, now: string): Promise<Fact>;
43
+ export declare function removeFact(file: FactsFile, key: string): boolean;
@@ -0,0 +1,195 @@
1
+ import { promises as fs } from 'node:fs';
2
+ import { createHash } from 'node:crypto';
3
+ import { DevWorkspaceError } from '../errors/index.js';
4
+ import { SCHEMA_VERSION, validateFacts, } from '../config/schemas.js';
5
+ import { pathExists, sha256File } from '../utils/fs.js';
6
+ import { findAbsolutePaths, resolveInside, validateRelativePath } from '../utils/paths.js';
7
+ import { containsSecret } from '../security/redact.js';
8
+ import { isSensitiveFile } from '../security/files.js';
9
+ import { readVersioned, writeVersioned } from '../core/versioned.js';
10
+ export const MISSING_HASH = 'MISSING';
11
+ export function emptyFacts() {
12
+ return { schemaVersion: SCHEMA_VERSION, facts: [] };
13
+ }
14
+ export async function loadFacts(ws) {
15
+ if (!(await pathExists(ws.facts)))
16
+ return emptyFacts();
17
+ return (await readVersioned(ws.facts, 'facts', SCHEMA_VERSION, validateFacts)).data;
18
+ }
19
+ export async function saveFacts(ws, file) {
20
+ validateFacts(file);
21
+ await writeVersioned(ws, ws.facts, file);
22
+ }
23
+ /**
24
+ * Authority rank. Mirrors the hierarchy: user decisions > current repository evidence >
25
+ * verified facts > AI-derived findings > stale/unknown. Lower-ranked input never silently replaces a higher-ranked fact.
26
+ */
27
+ export function authorityRank(f) {
28
+ if (f.status === 'USER_DECISION')
29
+ return 5;
30
+ if (f.status === 'DETECTED' && f.sourceType !== 'ai')
31
+ return 4;
32
+ if (f.status === 'VERIFIED')
33
+ return f.sourceType === 'ai' ? 3 : 4;
34
+ if (f.status === 'INFERRED')
35
+ return 2;
36
+ return 1;
37
+ }
38
+ function defaultStatus(t) {
39
+ if (t === 'user')
40
+ return 'USER_DECISION';
41
+ if (t === 'ai')
42
+ return 'INFERRED';
43
+ return 'DETECTED';
44
+ }
45
+ export function factId(key) {
46
+ return 'f-' + createHash('sha256').update(key).digest('hex').slice(0, 10);
47
+ }
48
+ export async function hashSources(root, sources, cache = new Map()) {
49
+ const out = {};
50
+ for (const s of sources) {
51
+ let h = cache.get(s);
52
+ if (h === undefined) {
53
+ const full = resolveInside(root, s);
54
+ try {
55
+ const st = await fs.lstat(full);
56
+ h = st.isFile() ? await sha256File(full) : MISSING_HASH;
57
+ }
58
+ catch {
59
+ h = MISSING_HASH;
60
+ }
61
+ cache.set(s, h);
62
+ }
63
+ out[s] = h;
64
+ }
65
+ return out;
66
+ }
67
+ const KEY_RE = /^[a-z0-9][a-z0-9._-]{0,99}$/;
68
+ export async function buildFact(root, input, now, existing) {
69
+ if (!KEY_RE.test(input.key)) {
70
+ throw new DevWorkspaceError('USAGE', `Invalid fact key "${input.key}".`, {
71
+ hint: 'Use lowercase letters, digits, dots, dashes (e.g. "auth.session-store").',
72
+ });
73
+ }
74
+ if (input.value.trim() === '')
75
+ throw new DevWorkspaceError('USAGE', 'Fact value must not be empty.');
76
+ if (input.value.length > 4000)
77
+ throw new DevWorkspaceError('USAGE', 'Fact value is too long (max 4000 characters).', { hint: 'Store a concise summary; link to the source file instead.' });
78
+ if (containsSecret(input.value) || containsSecret(input.source)) {
79
+ throw new DevWorkspaceError('SECURITY', 'Fact looks like it contains a secret; refusing to store it.', {
80
+ hint: 'Describe where the secret is configured, never its value.',
81
+ });
82
+ }
83
+ const abs = findAbsolutePaths(input.value);
84
+ if (abs.length > 0) {
85
+ throw new DevWorkspaceError('UNSAFE_PATH', `Fact value contains a machine-specific absolute path (${abs[0]}).`, {
86
+ hint: 'Use project-relative paths so knowledge stays portable.',
87
+ });
88
+ }
89
+ const sources = [...new Set(input.sources ?? [])];
90
+ for (const s of sources) {
91
+ const err = validateRelativePath(s);
92
+ if (err)
93
+ throw new DevWorkspaceError('UNSAFE_PATH', `Invalid fact source "${s}" (${err}).`);
94
+ if (isSensitiveFile(s))
95
+ throw new DevWorkspaceError('SECURITY', `Fact source "${s}" is a sensitive file.`);
96
+ }
97
+ const status = input.status ?? defaultStatus(input.sourceType);
98
+ const hashes = await hashSources(root, sources);
99
+ const missing = sources.filter((s) => hashes[s] === MISSING_HASH);
100
+ if (missing.length > 0 && input.sourceType !== 'user') {
101
+ throw new DevWorkspaceError('NOT_FOUND', `Fact source file(s) not found: ${missing.join(', ')}`, {
102
+ hint: 'Cite files that exist in the repository.',
103
+ });
104
+ }
105
+ return {
106
+ id: factId(input.key),
107
+ key: input.key,
108
+ value: input.value.trim(),
109
+ source: input.source,
110
+ sourceType: input.sourceType,
111
+ sources,
112
+ sourceHashes: hashes,
113
+ confidence: input.confidence,
114
+ status,
115
+ createdAt: existing?.createdAt ?? now,
116
+ updatedAt: now,
117
+ lastVerifiedAt: status === 'VERIFIED' || status === 'DETECTED' || status === 'USER_DECISION' ? now : null,
118
+ };
119
+ }
120
+ export function upsertFact(file, fact) {
121
+ const i = file.facts.findIndex((f) => f.key === fact.key);
122
+ if (i === -1) {
123
+ file.facts.push(fact);
124
+ return 'created';
125
+ }
126
+ const existing = file.facts[i];
127
+ if (existing.status !== 'STALE' && authorityRank(fact) < authorityRank(existing)) {
128
+ throw new DevWorkspaceError('CONFLICT', `Fact "${fact.key}" is held at higher authority (${existing.status}); refusing to overwrite it.`, {
129
+ hint: 'Ask the user to confirm the change, then record it as a user decision.',
130
+ });
131
+ }
132
+ fact.createdAt = existing.createdAt;
133
+ const same = existing.value === fact.value &&
134
+ existing.status === fact.status &&
135
+ existing.confidence === fact.confidence &&
136
+ JSON.stringify(existing.sourceHashes) === JSON.stringify(fact.sourceHashes);
137
+ // Leave an identical fact byte-for-byte alone so repeated analysis never churns version control.
138
+ if (same)
139
+ return 'unchanged';
140
+ file.facts[i] = fact;
141
+ return 'updated';
142
+ }
143
+ /** Compare stored source hashes with the repository now. Mutates facts that became stale. */
144
+ export async function applyStaleness(root, file, now) {
145
+ const cache = new Map();
146
+ const report = { checked: 0, newlyStale: [], alreadyStale: [], needsReview: [] };
147
+ for (const f of file.facts) {
148
+ if (f.status === 'STALE') {
149
+ report.alreadyStale.push(f.key);
150
+ continue;
151
+ }
152
+ if (f.sources.length === 0)
153
+ continue;
154
+ report.checked++;
155
+ const current = await hashSources(root, f.sources, cache);
156
+ const changed = f.sources.filter((s) => current[s] !== f.sourceHashes[s]);
157
+ if (changed.length === 0)
158
+ continue;
159
+ const reason = changed.map((s) => `${s} ${current[s] === MISSING_HASH ? 'removed' : 'changed'}`).join('; ');
160
+ if (f.status === 'USER_DECISION') {
161
+ report.needsReview.push({ key: f.key, reason });
162
+ continue;
163
+ }
164
+ f.status = 'STALE';
165
+ f.staleReason = reason;
166
+ f.updatedAt = now;
167
+ report.newlyStale.push({ key: f.key, reason });
168
+ }
169
+ return report;
170
+ }
171
+ /** Re-hash a fact's sources and mark it verified (called after the agent re-checks it against the repository). */
172
+ export async function verifyFact(root, file, key, now) {
173
+ const f = file.facts.find((x) => x.key === key);
174
+ if (!f)
175
+ throw new DevWorkspaceError('NOT_FOUND', `No fact with key "${key}".`);
176
+ f.sourceHashes = await hashSources(root, f.sources);
177
+ const gone = f.sources.filter((s) => f.sourceHashes[s] === MISSING_HASH);
178
+ if (gone.length > 0) {
179
+ throw new DevWorkspaceError('NOT_FOUND', `Cannot verify "${key}": source(s) missing: ${gone.join(', ')}`, {
180
+ hint: 'Update the fact with new sources or remove it.',
181
+ });
182
+ }
183
+ if (f.status !== 'USER_DECISION')
184
+ f.status = f.sourceType === 'detector' ? 'DETECTED' : 'VERIFIED';
185
+ delete f.staleReason;
186
+ f.lastVerifiedAt = now;
187
+ f.updatedAt = now;
188
+ return f;
189
+ }
190
+ export function removeFact(file, key) {
191
+ const n = file.facts.length;
192
+ file.facts = file.facts.filter((f) => f.key !== key);
193
+ return file.facts.length !== n;
194
+ }
195
+ //# sourceMappingURL=facts.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"facts.js","sourceRoot":"","sources":["../../src/knowledge/facts.ts"],"names":[],"mappings":"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","sourcesContent":["import { promises as fs } from 'node:fs';\nimport { createHash } from 'node:crypto';\nimport { DevWorkspaceError } from '../errors/index.js';\nimport {\n SCHEMA_VERSION,\n validateFacts,\n type Confidence,\n type Fact,\n type FactsFile,\n type FactStatus,\n type SourceType,\n} from '../config/schemas.js';\nimport { pathExists, sha256File } from '../utils/fs.js';\nimport { findAbsolutePaths, resolveInside, validateRelativePath } from '../utils/paths.js';\nimport { containsSecret } from '../security/redact.js';\nimport { isSensitiveFile } from '../security/files.js';\nimport { readVersioned, writeVersioned } from '../core/versioned.js';\nimport type { Ws } from '../core/workspace.js';\n\nexport interface FactInput {\n key: string;\n value: string;\n source: string;\n sourceType: SourceType;\n sources?: string[];\n confidence: Confidence;\n status?: FactStatus;\n}\n\nexport const MISSING_HASH = 'MISSING';\n\nexport function emptyFacts(): FactsFile {\n return { schemaVersion: SCHEMA_VERSION, facts: [] };\n}\n\nexport async function loadFacts(ws: Ws): Promise<FactsFile> {\n if (!(await pathExists(ws.facts))) return emptyFacts();\n return (await readVersioned(ws.facts, 'facts', SCHEMA_VERSION, validateFacts)).data;\n}\n\nexport async function saveFacts(ws: Ws, file: FactsFile): Promise<void> {\n validateFacts(file);\n await writeVersioned(ws, ws.facts, file);\n}\n\n/**\n * Authority rank. Mirrors the hierarchy: user decisions > current repository evidence >\n * verified facts > AI-derived findings > stale/unknown. Lower-ranked input never silently replaces a higher-ranked fact.\n */\nexport function authorityRank(f: Pick<Fact, 'status' | 'sourceType'>): number {\n if (f.status === 'USER_DECISION') return 5;\n if (f.status === 'DETECTED' && f.sourceType !== 'ai') return 4;\n if (f.status === 'VERIFIED') return f.sourceType === 'ai' ? 3 : 4;\n if (f.status === 'INFERRED') return 2;\n return 1;\n}\n\nfunction defaultStatus(t: SourceType): FactStatus {\n if (t === 'user') return 'USER_DECISION';\n if (t === 'ai') return 'INFERRED';\n return 'DETECTED';\n}\n\nexport function factId(key: string): string {\n return 'f-' + createHash('sha256').update(key).digest('hex').slice(0, 10);\n}\n\nexport async function hashSources(root: string, sources: string[], cache: Map<string, string> = new Map()): Promise<Record<string, string>> {\n const out: Record<string, string> = {};\n for (const s of sources) {\n let h = cache.get(s);\n if (h === undefined) {\n const full = resolveInside(root, s);\n try {\n const st = await fs.lstat(full);\n h = st.isFile() ? await sha256File(full) : MISSING_HASH;\n } catch {\n h = MISSING_HASH;\n }\n cache.set(s, h);\n }\n out[s] = h;\n }\n return out;\n}\n\nconst KEY_RE = /^[a-z0-9][a-z0-9._-]{0,99}$/;\n\nexport async function buildFact(root: string, input: FactInput, now: string, existing?: Fact): Promise<Fact> {\n if (!KEY_RE.test(input.key)) {\n throw new DevWorkspaceError('USAGE', `Invalid fact key \"${input.key}\".`, {\n hint: 'Use lowercase letters, digits, dots, dashes (e.g. \"auth.session-store\").',\n });\n }\n if (input.value.trim() === '') throw new DevWorkspaceError('USAGE', 'Fact value must not be empty.');\n if (input.value.length > 4000) throw new DevWorkspaceError('USAGE', 'Fact value is too long (max 4000 characters).', { hint: 'Store a concise summary; link to the source file instead.' });\n if (containsSecret(input.value) || containsSecret(input.source)) {\n throw new DevWorkspaceError('SECURITY', 'Fact looks like it contains a secret; refusing to store it.', {\n hint: 'Describe where the secret is configured, never its value.',\n });\n }\n const abs = findAbsolutePaths(input.value);\n if (abs.length > 0) {\n throw new DevWorkspaceError('UNSAFE_PATH', `Fact value contains a machine-specific absolute path (${abs[0]}).`, {\n hint: 'Use project-relative paths so knowledge stays portable.',\n });\n }\n const sources = [...new Set(input.sources ?? [])];\n for (const s of sources) {\n const err = validateRelativePath(s);\n if (err) throw new DevWorkspaceError('UNSAFE_PATH', `Invalid fact source \"${s}\" (${err}).`);\n if (isSensitiveFile(s)) throw new DevWorkspaceError('SECURITY', `Fact source \"${s}\" is a sensitive file.`);\n }\n const status = input.status ?? defaultStatus(input.sourceType);\n const hashes = await hashSources(root, sources);\n const missing = sources.filter((s) => hashes[s] === MISSING_HASH);\n if (missing.length > 0 && input.sourceType !== 'user') {\n throw new DevWorkspaceError('NOT_FOUND', `Fact source file(s) not found: ${missing.join(', ')}`, {\n hint: 'Cite files that exist in the repository.',\n });\n }\n return {\n id: factId(input.key),\n key: input.key,\n value: input.value.trim(),\n source: input.source,\n sourceType: input.sourceType,\n sources,\n sourceHashes: hashes,\n confidence: input.confidence,\n status,\n createdAt: existing?.createdAt ?? now,\n updatedAt: now,\n lastVerifiedAt: status === 'VERIFIED' || status === 'DETECTED' || status === 'USER_DECISION' ? now : null,\n };\n}\n\nexport type UpsertAction = 'created' | 'updated' | 'unchanged';\n\nexport function upsertFact(file: FactsFile, fact: Fact): UpsertAction {\n const i = file.facts.findIndex((f) => f.key === fact.key);\n if (i === -1) {\n file.facts.push(fact);\n return 'created';\n }\n const existing = file.facts[i]!;\n if (existing.status !== 'STALE' && authorityRank(fact) < authorityRank(existing)) {\n throw new DevWorkspaceError('CONFLICT', `Fact \"${fact.key}\" is held at higher authority (${existing.status}); refusing to overwrite it.`, {\n hint: 'Ask the user to confirm the change, then record it as a user decision.',\n });\n }\n fact.createdAt = existing.createdAt;\n const same =\n existing.value === fact.value &&\n existing.status === fact.status &&\n existing.confidence === fact.confidence &&\n JSON.stringify(existing.sourceHashes) === JSON.stringify(fact.sourceHashes);\n // Leave an identical fact byte-for-byte alone so repeated analysis never churns version control.\n if (same) return 'unchanged';\n file.facts[i] = fact;\n return 'updated';\n}\n\nexport interface StalenessReport {\n checked: number;\n newlyStale: { key: string; reason: string }[];\n alreadyStale: string[];\n /** User decisions whose sources changed. They are never auto-staled; a human should review them. */\n needsReview: { key: string; reason: string }[];\n}\n\n/** Compare stored source hashes with the repository now. Mutates facts that became stale. */\nexport async function applyStaleness(root: string, file: FactsFile, now: string): Promise<StalenessReport> {\n const cache = new Map<string, string>();\n const report: StalenessReport = { checked: 0, newlyStale: [], alreadyStale: [], needsReview: [] };\n for (const f of file.facts) {\n if (f.status === 'STALE') {\n report.alreadyStale.push(f.key);\n continue;\n }\n if (f.sources.length === 0) continue;\n report.checked++;\n const current = await hashSources(root, f.sources, cache);\n const changed = f.sources.filter((s) => current[s] !== f.sourceHashes[s]);\n if (changed.length === 0) continue;\n const reason = changed.map((s) => `${s} ${current[s] === MISSING_HASH ? 'removed' : 'changed'}`).join('; ');\n if (f.status === 'USER_DECISION') {\n report.needsReview.push({ key: f.key, reason });\n continue;\n }\n f.status = 'STALE';\n f.staleReason = reason;\n f.updatedAt = now;\n report.newlyStale.push({ key: f.key, reason });\n }\n return report;\n}\n\n/** Re-hash a fact's sources and mark it verified (called after the agent re-checks it against the repository). */\nexport async function verifyFact(root: string, file: FactsFile, key: string, now: string): Promise<Fact> {\n const f = file.facts.find((x) => x.key === key);\n if (!f) throw new DevWorkspaceError('NOT_FOUND', `No fact with key \"${key}\".`);\n f.sourceHashes = await hashSources(root, f.sources);\n const gone = f.sources.filter((s) => f.sourceHashes[s] === MISSING_HASH);\n if (gone.length > 0) {\n throw new DevWorkspaceError('NOT_FOUND', `Cannot verify \"${key}\": source(s) missing: ${gone.join(', ')}`, {\n hint: 'Update the fact with new sources or remove it.',\n });\n }\n if (f.status !== 'USER_DECISION') f.status = f.sourceType === 'detector' ? 'DETECTED' : 'VERIFIED';\n delete f.staleReason;\n f.lastVerifiedAt = now;\n f.updatedAt = now;\n return f;\n}\n\nexport function removeFact(file: FactsFile, key: string): boolean {\n const n = file.facts.length;\n file.facts = file.facts.filter((f) => f.key !== key);\n return file.facts.length !== n;\n}\n"]}
@@ -0,0 +1,22 @@
1
+ export declare const BUNDLE_FORMAT = "devworkspace-export";
2
+ export declare const MAX_BUNDLE_BYTES: number;
3
+ export declare const MAX_FILE_BYTES: number;
4
+ export declare const MAX_FILES = 5000;
5
+ export interface Bundle {
6
+ format: typeof BUNDLE_FORMAT;
7
+ schemaVersion: number;
8
+ exportedAt: string;
9
+ devworkspaceVersion: string;
10
+ projectId: string;
11
+ projectName: string;
12
+ files: Record<string, string>;
13
+ }
14
+ export declare function isPortablePath(rel: string): boolean;
15
+ /** Workspace files that may go into a bundle (excludes machine-local state and regenerable prompts). */
16
+ export declare function isExportable(rel: string): boolean;
17
+ export interface BundleProblem {
18
+ path: string;
19
+ problem: string;
20
+ }
21
+ /** Strictly validate an untrusted bundle. Returns the typed bundle or throws with every problem found. */
22
+ export declare function parseBundle(raw: string): Bundle;
@@ -0,0 +1,102 @@
1
+ import { DevWorkspaceError } from '../errors/index.js';
2
+ import { SCHEMA_VERSION } from '../config/schemas.js';
3
+ import { validateRelativePath } from '../utils/paths.js';
4
+ import { findSecrets } from '../security/redact.js';
5
+ import { isSensitiveFile } from '../security/files.js';
6
+ import { migrateData } from '../core/migrations.js';
7
+ export const BUNDLE_FORMAT = 'devworkspace-export';
8
+ export const MAX_BUNDLE_BYTES = 25 * 1024 * 1024;
9
+ export const MAX_FILE_BYTES = 1024 * 1024;
10
+ export const MAX_FILES = 5000;
11
+ /** Workspace-relative paths that may travel in a bundle. Anything else is rejected on import. */
12
+ const ALLOWED = [
13
+ /^project\.json$/,
14
+ /^context\/[A-Za-z0-9._-]+\.(?:json|md)$/,
15
+ /^decisions\/[A-Za-z0-9._-]+\.md$/,
16
+ /^tasks\/(?:active|completed)\/[A-Za-z0-9._-]+\.json$/,
17
+ /^sessions\/[A-Za-z0-9._-]+\.json$/,
18
+ /^analysis\/discovery\.json$/,
19
+ ];
20
+ export function isPortablePath(rel) {
21
+ return ALLOWED.some((re) => re.test(rel));
22
+ }
23
+ /** Workspace files that may go into a bundle (excludes machine-local state and regenerable prompts). */
24
+ export function isExportable(rel) {
25
+ return isPortablePath(rel) && !isSensitiveFile(rel);
26
+ }
27
+ /** Strictly validate an untrusted bundle. Returns the typed bundle or throws with every problem found. */
28
+ export function parseBundle(raw) {
29
+ if (raw.length > MAX_BUNDLE_BYTES) {
30
+ throw new DevWorkspaceError('SECURITY', `Bundle is larger than the ${MAX_BUNDLE_BYTES / 1024 / 1024} MB limit.`);
31
+ }
32
+ let data;
33
+ try {
34
+ data = JSON.parse(raw.replace(/^\uFEFF/, ''));
35
+ }
36
+ catch (e) {
37
+ throw new DevWorkspaceError('SCHEMA', 'The import file is not valid JSON.', { cause: e });
38
+ }
39
+ if (typeof data !== 'object' || data === null || Array.isArray(data))
40
+ throw new DevWorkspaceError('SCHEMA', 'The import file is not a DevWorkspace export.');
41
+ const b = data;
42
+ if (b.format !== BUNDLE_FORMAT)
43
+ throw new DevWorkspaceError('SCHEMA', `Not a DevWorkspace export (format "${String(b.format)}").`);
44
+ if (typeof b.schemaVersion !== 'number' || !Number.isInteger(b.schemaVersion))
45
+ throw new DevWorkspaceError('SCHEMA', 'Export has no schemaVersion.');
46
+ if (b.schemaVersion > SCHEMA_VERSION) {
47
+ throw new DevWorkspaceError('SCHEMA_TOO_NEW', `Export uses schema v${b.schemaVersion}; this DevWorkspace understands up to v${SCHEMA_VERSION}.`, {
48
+ hint: 'Upgrade DevWorkspace, then retry.',
49
+ });
50
+ }
51
+ for (const k of ['exportedAt', 'devworkspaceVersion', 'projectId', 'projectName']) {
52
+ if (typeof b[k] !== 'string' || b[k].length > 300)
53
+ throw new DevWorkspaceError('SCHEMA', `Export field "${k}" is missing or invalid.`);
54
+ }
55
+ if (typeof b.files !== 'object' || b.files === null || Array.isArray(b.files))
56
+ throw new DevWorkspaceError('SCHEMA', 'Export has no files map.');
57
+ const files = b.files;
58
+ const names = Object.keys(files);
59
+ if (names.length > MAX_FILES)
60
+ throw new DevWorkspaceError('SECURITY', `Export contains too many files (${names.length}).`);
61
+ const problems = [];
62
+ for (const name of names) {
63
+ const content = files[name];
64
+ const pathErr = validateRelativePath(name);
65
+ if (pathErr) {
66
+ problems.push({ path: name, problem: `unsafe path (${pathErr})` });
67
+ continue;
68
+ }
69
+ if (!isPortablePath(name)) {
70
+ problems.push({ path: name, problem: 'path is not an allowed workspace file' });
71
+ continue;
72
+ }
73
+ if (typeof content !== 'string') {
74
+ problems.push({ path: name, problem: 'content is not text' });
75
+ continue;
76
+ }
77
+ if (content.length > MAX_FILE_BYTES) {
78
+ problems.push({ path: name, problem: 'file too large' });
79
+ continue;
80
+ }
81
+ if (findSecrets(content).length > 0) {
82
+ problems.push({ path: name, problem: 'contains secret-like values' });
83
+ continue;
84
+ }
85
+ if (name.endsWith('.json')) {
86
+ try {
87
+ const parsed = JSON.parse(content);
88
+ migrateData(parsed, SCHEMA_VERSION, [], name);
89
+ }
90
+ catch (e) {
91
+ problems.push({ path: name, problem: `invalid JSON content: ${e.message.split('\n')[0]}` });
92
+ }
93
+ }
94
+ }
95
+ if (problems.length > 0) {
96
+ throw new DevWorkspaceError('SECURITY', `Import rejected; nothing was changed:\n - ${problems.slice(0, 15).map((p) => `${p.path}: ${p.problem}`).join('\n - ')}`, {
97
+ hint: 'Only import bundles you trust, produced by "devworkspace export".',
98
+ });
99
+ }
100
+ return b;
101
+ }
102
+ //# sourceMappingURL=bundle.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bundle.js","sourceRoot":"","sources":["../../src/portability/bundle.ts"],"names":[],"mappings":"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","sourcesContent":["import { DevWorkspaceError } from '../errors/index.js';\nimport { SCHEMA_VERSION } from '../config/schemas.js';\nimport { validateRelativePath } from '../utils/paths.js';\nimport { findSecrets } from '../security/redact.js';\nimport { isSensitiveFile } from '../security/files.js';\nimport { migrateData } from '../core/migrations.js';\n\nexport const BUNDLE_FORMAT = 'devworkspace-export';\nexport const MAX_BUNDLE_BYTES = 25 * 1024 * 1024;\nexport const MAX_FILE_BYTES = 1024 * 1024;\nexport const MAX_FILES = 5000;\n\nexport interface Bundle {\n format: typeof BUNDLE_FORMAT;\n schemaVersion: number;\n exportedAt: string;\n devworkspaceVersion: string;\n projectId: string;\n projectName: string;\n files: Record<string, string>;\n}\n\n/** Workspace-relative paths that may travel in a bundle. Anything else is rejected on import. */\nconst ALLOWED: RegExp[] = [\n /^project\\.json$/,\n /^context\\/[A-Za-z0-9._-]+\\.(?:json|md)$/,\n /^decisions\\/[A-Za-z0-9._-]+\\.md$/,\n /^tasks\\/(?:active|completed)\\/[A-Za-z0-9._-]+\\.json$/,\n /^sessions\\/[A-Za-z0-9._-]+\\.json$/,\n /^analysis\\/discovery\\.json$/,\n];\n\nexport function isPortablePath(rel: string): boolean {\n return ALLOWED.some((re) => re.test(rel));\n}\n\n/** Workspace files that may go into a bundle (excludes machine-local state and regenerable prompts). */\nexport function isExportable(rel: string): boolean {\n return isPortablePath(rel) && !isSensitiveFile(rel);\n}\n\nexport interface BundleProblem {\n path: string;\n problem: string;\n}\n\n/** Strictly validate an untrusted bundle. Returns the typed bundle or throws with every problem found. */\nexport function parseBundle(raw: string): Bundle {\n if (raw.length > MAX_BUNDLE_BYTES) {\n throw new DevWorkspaceError('SECURITY', `Bundle is larger than the ${MAX_BUNDLE_BYTES / 1024 / 1024} MB limit.`);\n }\n let data: unknown;\n try {\n data = JSON.parse(raw.replace(/^\\uFEFF/, ''));\n } catch (e) {\n throw new DevWorkspaceError('SCHEMA', 'The import file is not valid JSON.', { cause: e });\n }\n if (typeof data !== 'object' || data === null || Array.isArray(data)) throw new DevWorkspaceError('SCHEMA', 'The import file is not a DevWorkspace export.');\n const b = data as Record<string, unknown>;\n if (b.format !== BUNDLE_FORMAT) throw new DevWorkspaceError('SCHEMA', `Not a DevWorkspace export (format \"${String(b.format)}\").`);\n if (typeof b.schemaVersion !== 'number' || !Number.isInteger(b.schemaVersion)) throw new DevWorkspaceError('SCHEMA', 'Export has no schemaVersion.');\n if (b.schemaVersion > SCHEMA_VERSION) {\n throw new DevWorkspaceError('SCHEMA_TOO_NEW', `Export uses schema v${b.schemaVersion}; this DevWorkspace understands up to v${SCHEMA_VERSION}.`, {\n hint: 'Upgrade DevWorkspace, then retry.',\n });\n }\n for (const k of ['exportedAt', 'devworkspaceVersion', 'projectId', 'projectName'] as const) {\n if (typeof b[k] !== 'string' || (b[k] as string).length > 300) throw new DevWorkspaceError('SCHEMA', `Export field \"${k}\" is missing or invalid.`);\n }\n if (typeof b.files !== 'object' || b.files === null || Array.isArray(b.files)) throw new DevWorkspaceError('SCHEMA', 'Export has no files map.');\n const files = b.files as Record<string, unknown>;\n const names = Object.keys(files);\n if (names.length > MAX_FILES) throw new DevWorkspaceError('SECURITY', `Export contains too many files (${names.length}).`);\n\n const problems: BundleProblem[] = [];\n for (const name of names) {\n const content = files[name];\n const pathErr = validateRelativePath(name);\n if (pathErr) {\n problems.push({ path: name, problem: `unsafe path (${pathErr})` });\n continue;\n }\n if (!isPortablePath(name)) {\n problems.push({ path: name, problem: 'path is not an allowed workspace file' });\n continue;\n }\n if (typeof content !== 'string') {\n problems.push({ path: name, problem: 'content is not text' });\n continue;\n }\n if (content.length > MAX_FILE_BYTES) {\n problems.push({ path: name, problem: 'file too large' });\n continue;\n }\n if (findSecrets(content).length > 0) {\n problems.push({ path: name, problem: 'contains secret-like values' });\n continue;\n }\n if (name.endsWith('.json')) {\n try {\n const parsed = JSON.parse(content) as unknown;\n migrateData(parsed, SCHEMA_VERSION, [], name);\n } catch (e) {\n problems.push({ path: name, problem: `invalid JSON content: ${(e as Error).message.split('\\n')[0]}` });\n }\n }\n }\n if (problems.length > 0) {\n throw new DevWorkspaceError('SECURITY', `Import rejected; nothing was changed:\\n - ${problems.slice(0, 15).map((p) => `${p.path}: ${p.problem}`).join('\\n - ')}`, {\n hint: 'Only import bundles you trust, produced by \"devworkspace export\".',\n });\n }\n return b as unknown as Bundle;\n}\n"]}