crewly 1.8.9 → 1.8.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/config/constants.d.ts.map +1 -0
- package/config/index.d.ts.map +1 -0
- package/config/roles/_common/memory-instructions.md +6 -5
- package/config/roles/_common/wiki-instructions.md +49 -0
- package/config/roles/architect/prompt.md +2 -2
- package/config/roles/backend-developer/prompt.md +2 -2
- package/config/roles/designer/prompt.md +2 -2
- package/config/roles/developer/prompt.md +2 -2
- package/config/roles/frontend-developer/prompt.md +2 -2
- package/config/roles/fullstack-dev/prompt.md +2 -2
- package/config/roles/generalist/prompt.md +2 -2
- package/config/roles/ops/prompt.md +2 -2
- package/config/roles/orchestrator/prompt.md +135 -11
- package/config/roles/product-manager/prompt.md +2 -2
- package/config/roles/qa/prompt.md +2 -2
- package/config/roles/qa-engineer/prompt.md +2 -2
- package/config/roles/researcher/prompt.md +15 -6
- package/config/roles/sales/prompt.md +2 -2
- package/config/roles/support/prompt.md +2 -2
- package/config/roles/team-leader/prompt.md +17 -2
- package/config/roles/tpm/prompt.md +2 -2
- package/config/roles/ux-designer/prompt.md +2 -2
- package/config/skills/orchestrator/wiki-cleanup/SKILL.md +89 -0
- package/config/skills/orchestrator/wiki-cleanup/execute.sh +139 -0
- package/config/skills/orchestrator/wiki-lint/SKILL.md +75 -0
- package/config/skills/orchestrator/wiki-lint/execute.sh +66 -0
- package/config/skills/orchestrator/wiki-migrate/SKILL.md +103 -0
- package/config/skills/orchestrator/wiki-migrate/execute.sh +82 -0
- package/config/skills/orchestrator/wiki-process-queue/SKILL.md +9 -1
- package/dist/backend/backend/src/controllers/task-management/task-management.controller.d.ts +169 -0
- package/dist/backend/backend/src/controllers/task-management/task-management.controller.d.ts.map +1 -0
- package/dist/backend/backend/src/controllers/task-management/task-management.controller.js +1779 -0
- package/dist/backend/backend/src/controllers/task-management/task-management.controller.js.map +1 -0
- package/dist/backend/backend/src/controllers/task-pool/task-pool.controller.d.ts +18 -0
- package/dist/backend/backend/src/controllers/task-pool/task-pool.controller.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/task-pool/task-pool.controller.js +63 -0
- package/dist/backend/backend/src/controllers/task-pool/task-pool.controller.js.map +1 -1
- package/dist/backend/backend/src/controllers/task-pool/task-pool.routes.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/task-pool/task-pool.routes.js +5 -1
- package/dist/backend/backend/src/controllers/task-pool/task-pool.routes.js.map +1 -1
- package/dist/backend/backend/src/controllers/wiki/wiki.controller.d.ts +109 -0
- package/dist/backend/backend/src/controllers/wiki/wiki.controller.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/wiki/wiki.controller.js +418 -4
- package/dist/backend/backend/src/controllers/wiki/wiki.controller.js.map +1 -1
- package/dist/backend/backend/src/controllers/wiki/wiki.routes.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/wiki/wiki.routes.js +11 -1
- package/dist/backend/backend/src/controllers/wiki/wiki.routes.js.map +1 -1
- package/dist/backend/backend/src/index.d.ts.map +1 -1
- package/dist/backend/backend/src/index.js +64 -0
- package/dist/backend/backend/src/index.js.map +1 -1
- package/dist/backend/backend/src/index.js.orc-bak-20260529 +3130 -0
- package/dist/backend/backend/src/services/agent/crewly-agent/agent-runner.service.d.ts +513 -0
- package/dist/backend/backend/src/services/agent/crewly-agent/agent-runner.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/agent/crewly-agent/agent-runner.service.js +1568 -0
- package/dist/backend/backend/src/services/agent/crewly-agent/agent-runner.service.js.map +1 -0
- package/dist/backend/backend/src/services/agent/crewly-agent/agent-worker.d.ts +86 -0
- package/dist/backend/backend/src/services/agent/crewly-agent/agent-worker.d.ts.map +1 -0
- package/dist/backend/backend/src/services/agent/crewly-agent/agent-worker.js +147 -0
- package/dist/backend/backend/src/services/agent/crewly-agent/agent-worker.js.map +1 -0
- package/dist/backend/backend/src/services/agent/crewly-agent/api-client.d.ts +68 -0
- package/dist/backend/backend/src/services/agent/crewly-agent/api-client.d.ts.map +1 -0
- package/dist/backend/backend/src/services/agent/crewly-agent/api-client.js +131 -0
- package/dist/backend/backend/src/services/agent/crewly-agent/api-client.js.map +1 -0
- package/dist/backend/backend/src/services/agent/crewly-agent/audit-log.service.d.ts +130 -0
- package/dist/backend/backend/src/services/agent/crewly-agent/audit-log.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/agent/crewly-agent/audit-log.service.js +263 -0
- package/dist/backend/backend/src/services/agent/crewly-agent/audit-log.service.js.map +1 -0
- package/dist/backend/backend/src/services/agent/crewly-agent/audit-trail.service.d.ts +74 -0
- package/dist/backend/backend/src/services/agent/crewly-agent/audit-trail.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/agent/crewly-agent/audit-trail.service.js +140 -0
- package/dist/backend/backend/src/services/agent/crewly-agent/audit-trail.service.js.map +1 -0
- package/dist/backend/backend/src/services/agent/crewly-agent/auditor-tools.d.ts +29 -0
- package/dist/backend/backend/src/services/agent/crewly-agent/auditor-tools.d.ts.map +1 -0
- package/dist/backend/backend/src/services/agent/crewly-agent/auditor-tools.js +279 -0
- package/dist/backend/backend/src/services/agent/crewly-agent/auditor-tools.js.map +1 -0
- package/dist/backend/backend/src/services/agent/crewly-agent/crewly-agent-runtime.service.d.ts +340 -0
- package/dist/backend/backend/src/services/agent/crewly-agent/crewly-agent-runtime.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/agent/crewly-agent/crewly-agent-runtime.service.js +1176 -0
- package/dist/backend/backend/src/services/agent/crewly-agent/crewly-agent-runtime.service.js.map +1 -0
- package/dist/backend/backend/src/services/agent/crewly-agent/deepseek-sse-transform.d.ts +79 -0
- package/dist/backend/backend/src/services/agent/crewly-agent/deepseek-sse-transform.d.ts.map +1 -0
- package/dist/backend/backend/src/services/agent/crewly-agent/deepseek-sse-transform.js +145 -0
- package/dist/backend/backend/src/services/agent/crewly-agent/deepseek-sse-transform.js.map +1 -0
- package/dist/backend/backend/src/services/agent/crewly-agent/env-isolation.service.d.ts +79 -0
- package/dist/backend/backend/src/services/agent/crewly-agent/env-isolation.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/agent/crewly-agent/env-isolation.service.js +218 -0
- package/dist/backend/backend/src/services/agent/crewly-agent/env-isolation.service.js.map +1 -0
- package/dist/backend/backend/src/services/agent/crewly-agent/index.d.ts +16 -0
- package/dist/backend/backend/src/services/agent/crewly-agent/index.d.ts.map +1 -0
- package/dist/backend/backend/src/services/agent/crewly-agent/index.js +16 -0
- package/dist/backend/backend/src/services/agent/crewly-agent/index.js.map +1 -0
- package/dist/backend/backend/src/services/agent/crewly-agent/mcp-tool-bridge.d.ts +135 -0
- package/dist/backend/backend/src/services/agent/crewly-agent/mcp-tool-bridge.d.ts.map +1 -0
- package/dist/backend/backend/src/services/agent/crewly-agent/mcp-tool-bridge.js +185 -0
- package/dist/backend/backend/src/services/agent/crewly-agent/mcp-tool-bridge.js.map +1 -0
- package/dist/backend/backend/src/services/agent/crewly-agent/model-manager.d.ts +141 -0
- package/dist/backend/backend/src/services/agent/crewly-agent/model-manager.d.ts.map +1 -0
- package/dist/backend/backend/src/services/agent/crewly-agent/model-manager.js +310 -0
- package/dist/backend/backend/src/services/agent/crewly-agent/model-manager.js.map +1 -0
- package/dist/backend/backend/src/services/agent/crewly-agent/output-filter.service.d.ts +91 -0
- package/dist/backend/backend/src/services/agent/crewly-agent/output-filter.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/agent/crewly-agent/output-filter.service.js +143 -0
- package/dist/backend/backend/src/services/agent/crewly-agent/output-filter.service.js.map +1 -0
- package/dist/backend/backend/src/services/agent/crewly-agent/prompt-guard.service.d.ts +103 -0
- package/dist/backend/backend/src/services/agent/crewly-agent/prompt-guard.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/agent/crewly-agent/prompt-guard.service.js +256 -0
- package/dist/backend/backend/src/services/agent/crewly-agent/prompt-guard.service.js.map +1 -0
- package/dist/backend/backend/src/services/agent/crewly-agent/rate-limiter.d.ts +143 -0
- package/dist/backend/backend/src/services/agent/crewly-agent/rate-limiter.d.ts.map +1 -0
- package/dist/backend/backend/src/services/agent/crewly-agent/rate-limiter.js +264 -0
- package/dist/backend/backend/src/services/agent/crewly-agent/rate-limiter.js.map +1 -0
- package/dist/backend/backend/src/services/agent/crewly-agent/smoke-test.d.ts +13 -0
- package/dist/backend/backend/src/services/agent/crewly-agent/smoke-test.d.ts.map +1 -0
- package/dist/backend/backend/src/services/agent/crewly-agent/smoke-test.js +91 -0
- package/dist/backend/backend/src/services/agent/crewly-agent/smoke-test.js.map +1 -0
- package/dist/backend/backend/src/services/agent/crewly-agent/tool-registry.d.ts +135 -0
- package/dist/backend/backend/src/services/agent/crewly-agent/tool-registry.d.ts.map +1 -0
- package/dist/backend/backend/src/services/agent/crewly-agent/tool-registry.js +1937 -0
- package/dist/backend/backend/src/services/agent/crewly-agent/tool-registry.js.map +1 -0
- package/dist/backend/backend/src/services/ai/prompt-builder.service.js +1 -1
- package/dist/backend/backend/src/services/autonomous/auto-assign.service.d.ts +429 -0
- package/dist/backend/backend/src/services/autonomous/auto-assign.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/autonomous/auto-assign.service.js +852 -0
- package/dist/backend/backend/src/services/autonomous/auto-assign.service.js.map +1 -0
- package/dist/backend/backend/src/services/project/task-tracking.service.d.ts +171 -0
- package/dist/backend/backend/src/services/project/task-tracking.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/project/task-tracking.service.js +725 -0
- package/dist/backend/backend/src/services/project/task-tracking.service.js.map +1 -0
- package/dist/backend/backend/src/services/reconciler/reconciler-data-provider.d.ts.map +1 -1
- package/dist/backend/backend/src/services/reconciler/reconciler-data-provider.js +50 -0
- package/dist/backend/backend/src/services/reconciler/reconciler-data-provider.js.map +1 -1
- package/dist/backend/backend/src/services/task-pool/task-pool.service.d.ts +19 -0
- package/dist/backend/backend/src/services/task-pool/task-pool.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/task-pool/task-pool.service.js +45 -0
- package/dist/backend/backend/src/services/task-pool/task-pool.service.js.map +1 -1
- package/dist/backend/backend/src/services/v3/agent-auto-claim.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/v3/agent-auto-claim.service.js +34 -1
- package/dist/backend/backend/src/services/v3/agent-auto-claim.service.js.map +1 -1
- package/dist/backend/backend/src/services/v3/project-task-watcher.service.d.ts +118 -0
- package/dist/backend/backend/src/services/v3/project-task-watcher.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/v3/project-task-watcher.service.js +326 -0
- package/dist/backend/backend/src/services/v3/project-task-watcher.service.js.map +1 -0
- package/dist/backend/backend/src/services/wiki/wiki-backlinks.service.d.ts +72 -0
- package/dist/backend/backend/src/services/wiki/wiki-backlinks.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/wiki/wiki-backlinks.service.js +186 -0
- package/dist/backend/backend/src/services/wiki/wiki-backlinks.service.js.map +1 -0
- package/dist/backend/backend/src/services/wiki/wiki-bookkeep-trigger.service.d.ts +4 -1
- package/dist/backend/backend/src/services/wiki/wiki-bookkeep-trigger.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/wiki/wiki-bookkeep-trigger.service.js +24 -1
- package/dist/backend/backend/src/services/wiki/wiki-bookkeep-trigger.service.js.map +1 -1
- package/dist/backend/backend/src/services/wiki/wiki-chat-subscriber.service.d.ts +74 -0
- package/dist/backend/backend/src/services/wiki/wiki-chat-subscriber.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/wiki/wiki-chat-subscriber.service.js +154 -0
- package/dist/backend/backend/src/services/wiki/wiki-chat-subscriber.service.js.map +1 -0
- package/dist/backend/backend/src/services/wiki/wiki-cleanup.service.d.ts +160 -0
- package/dist/backend/backend/src/services/wiki/wiki-cleanup.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/wiki/wiki-cleanup.service.js +399 -0
- package/dist/backend/backend/src/services/wiki/wiki-cleanup.service.js.map +1 -0
- package/dist/backend/backend/src/services/wiki/wiki-lint.service.d.ts +182 -0
- package/dist/backend/backend/src/services/wiki/wiki-lint.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/wiki/wiki-lint.service.js +505 -0
- package/dist/backend/backend/src/services/wiki/wiki-lint.service.js.map +1 -0
- package/dist/backend/backend/src/services/wiki/wiki-migrate.service.d.ts +232 -0
- package/dist/backend/backend/src/services/wiki/wiki-migrate.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/wiki/wiki-migrate.service.js +1416 -0
- package/dist/backend/backend/src/services/wiki/wiki-migrate.service.js.map +1 -0
- package/dist/backend/backend/src/services/wiki/wiki-recent.service.d.ts +51 -0
- package/dist/backend/backend/src/services/wiki/wiki-recent.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/wiki/wiki-recent.service.js +102 -0
- package/dist/backend/backend/src/services/wiki/wiki-recent.service.js.map +1 -0
- package/dist/backend/backend/src/services/wiki/wiki-reflect-trigger.service.d.ts +84 -0
- package/dist/backend/backend/src/services/wiki/wiki-reflect-trigger.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/wiki/wiki-reflect-trigger.service.js +156 -0
- package/dist/backend/backend/src/services/wiki/wiki-reflect-trigger.service.js.map +1 -0
- package/dist/backend/backend/src/services/wiki/wiki-search.service.d.ts +90 -0
- package/dist/backend/backend/src/services/wiki/wiki-search.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/wiki/wiki-search.service.js +190 -0
- package/dist/backend/backend/src/services/wiki/wiki-search.service.js.map +1 -0
- package/dist/backend/backend/src/services/wiki/wiki-workitem-bridge.service.d.ts +164 -0
- package/dist/backend/backend/src/services/wiki/wiki-workitem-bridge.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/wiki/wiki-workitem-bridge.service.js +675 -0
- package/dist/backend/backend/src/services/wiki/wiki-workitem-bridge.service.js.map +1 -0
- package/dist/backend/backend/src/services/workflow/cron-task.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/workflow/cron-task.service.js +65 -0
- package/dist/backend/backend/src/services/workflow/cron-task.service.js.map +1 -1
- package/dist/backend/backend/src/types/auto-assign.types.d.ts +271 -0
- package/dist/backend/backend/src/types/auto-assign.types.d.ts.map +1 -0
- package/dist/backend/backend/src/types/auto-assign.types.js +136 -0
- package/dist/backend/backend/src/types/auto-assign.types.js.map +1 -0
- package/dist/backend/backend/src/types/cron-task.types.d.ts +16 -1
- package/dist/backend/backend/src/types/cron-task.types.d.ts.map +1 -1
- package/dist/backend/backend/src/utils/esm-require.utils.d.ts +111 -0
- package/dist/backend/backend/src/utils/esm-require.utils.d.ts.map +1 -0
- package/dist/backend/backend/src/utils/esm-require.utils.js +124 -0
- package/dist/backend/backend/src/utils/esm-require.utils.js.map +1 -0
- package/dist/cli/backend/src/services/ai/prompt-modules/prompt-module.interface.d.ts +220 -0
- package/dist/cli/backend/src/services/ai/prompt-modules/prompt-module.interface.d.ts.map +1 -0
- package/dist/cli/backend/src/services/ai/prompt-modules/prompt-module.interface.js +37 -0
- package/dist/cli/backend/src/services/ai/prompt-modules/prompt-module.interface.js.map +1 -0
- package/dist/cli/backend/src/services/knowledge/fts5-search-strategy.d.ts +56 -0
- package/dist/cli/backend/src/services/knowledge/fts5-search-strategy.d.ts.map +1 -0
- package/dist/cli/backend/src/services/knowledge/fts5-search-strategy.js +91 -0
- package/dist/cli/backend/src/services/knowledge/fts5-search-strategy.js.map +1 -0
- package/dist/cli/backend/src/services/knowledge/learnings-index.service.d.ts +159 -0
- package/dist/cli/backend/src/services/knowledge/learnings-index.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/knowledge/learnings-index.service.js +304 -0
- package/dist/cli/backend/src/services/knowledge/learnings-index.service.js.map +1 -0
- package/dist/cli/backend/src/services/knowledge/wiki-compiler.service.d.ts +115 -0
- package/dist/cli/backend/src/services/knowledge/wiki-compiler.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/knowledge/wiki-compiler.service.js +215 -0
- package/dist/cli/backend/src/services/knowledge/wiki-compiler.service.js.map +1 -0
- package/dist/cli/backend/src/services/memory/embedding-provider.d.ts +78 -0
- package/dist/cli/backend/src/services/memory/embedding-provider.d.ts.map +1 -0
- package/dist/cli/backend/src/services/memory/embedding-provider.js +179 -0
- package/dist/cli/backend/src/services/memory/embedding-provider.js.map +1 -0
- package/dist/cli/backend/src/services/memory/vector-store.service.d.ts +331 -0
- package/dist/cli/backend/src/services/memory/vector-store.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/memory/vector-store.service.js +814 -0
- package/dist/cli/backend/src/services/memory/vector-store.service.js.map +1 -0
- package/dist/cli/backend/src/services/project/task-tracking.service.d.ts +171 -0
- package/dist/cli/backend/src/services/project/task-tracking.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/project/task-tracking.service.js +725 -0
- package/dist/cli/backend/src/services/project/task-tracking.service.js.map +1 -0
- package/dist/cli/backend/src/services/task-pool/task-pool.service.d.ts +19 -0
- package/dist/cli/backend/src/services/task-pool/task-pool.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/task-pool/task-pool.service.js +45 -0
- package/dist/cli/backend/src/services/task-pool/task-pool.service.js.map +1 -1
- package/dist/cli/backend/src/types/auto-assign.types.d.ts +271 -0
- package/dist/cli/backend/src/types/auto-assign.types.d.ts.map +1 -0
- package/dist/cli/backend/src/types/auto-assign.types.js +136 -0
- package/dist/cli/backend/src/types/auto-assign.types.js.map +1 -0
- package/dist/cli/cli/src/index.js +0 -0
- package/frontend/dist/assets/{index-db3f5041.css → index-068bb4f6.css} +10 -1
- package/frontend/dist/assets/index-c24ceb15.js +4960 -0
- package/frontend/dist/index.html +2 -2
- package/package.json +1 -1
- package/config/skills/agent/core/query-knowledge/SKILL.md +0 -87
- package/config/skills/agent/core/query-knowledge/execute.sh +0 -30
- package/config/skills/orchestrator/query-knowledge/SKILL.md +0 -75
- package/config/skills/orchestrator/query-knowledge/execute.sh +0 -30
- package/frontend/dist/assets/index-cc115bb4.js +0 -4926
|
@@ -0,0 +1,1416 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WikiMigrateService — one-shot conversion from legacy OSS knowledge
|
|
3
|
+
* stores into the v2.1 three-scope vault structure.
|
|
4
|
+
*
|
|
5
|
+
* Legacy sources covered (per v2.1 spec §6 migration table):
|
|
6
|
+
* - `<project>/.crewly/knowledge/decisions.json[]` → llm-curated/decisions/
|
|
7
|
+
* - `<project>/.crewly/knowledge/patterns.json[]` → llm-curated/patterns/
|
|
8
|
+
* - `<project>/.crewly/knowledge/gotchas.json[]` → llm-curated/patterns/ (gotchas are a kind of pattern)
|
|
9
|
+
* - `<project>/.crewly/knowledge/relationships.json[]` → llm-curated/people/
|
|
10
|
+
* - `<project>/.crewly/knowledge/learnings.md` → append to llm-curated/log.md
|
|
11
|
+
* - `<project>/.crewly/knowledge/*.md` (loose) → llm-curated/decisions/ (or skipped per heuristic)
|
|
12
|
+
* - `~/.crewly/agents/<id>/memory.json roleKnowledge[]` → llm-curated/{patterns,decisions}/ (copy; original stays)
|
|
13
|
+
*
|
|
14
|
+
* Design rules (locked 2026-05-24):
|
|
15
|
+
* - Default DRY-RUN. Caller must pass `{ apply: true }` to write.
|
|
16
|
+
* - Idempotent. A manifest at `<vault>/.migration-state.json` records
|
|
17
|
+
* each migrated entry's content hash + source + target. Re-runs skip
|
|
18
|
+
* entries already in the manifest.
|
|
19
|
+
* - LEGACY FILES ARE NEVER DELETED. Rollback path stays open.
|
|
20
|
+
* - Routing: everything → project vault. Items that look cross-project
|
|
21
|
+
* (mention foreign team UUIDs / customer names) are tagged
|
|
22
|
+
* `routing_uncertain: true` in frontmatter; wiki-lint surfaces them
|
|
23
|
+
* later for human review.
|
|
24
|
+
* - Per-agent memory.json is COPIED, not moved. The agent's private
|
|
25
|
+
* memory store is preserved untouched.
|
|
26
|
+
*
|
|
27
|
+
* @module services/wiki/wiki-migrate.service
|
|
28
|
+
*/
|
|
29
|
+
import * as fs from 'fs/promises';
|
|
30
|
+
import * as path from 'path';
|
|
31
|
+
import * as os from 'os';
|
|
32
|
+
import { existsSync } from 'fs';
|
|
33
|
+
import { createHash } from 'crypto';
|
|
34
|
+
/** Filename inside the project vault where we record migrated entries. */
|
|
35
|
+
export const WIKI_MIGRATE_MANIFEST_FILENAME = '.migration-state.json';
|
|
36
|
+
/** Manifest format version. Bump on any breaking schema change. */
|
|
37
|
+
export const WIKI_MIGRATE_MANIFEST_VERSION = 1;
|
|
38
|
+
/** Hard cap on loose .md files we walk (defensive). */
|
|
39
|
+
export const WIKI_MIGRATE_MAX_LOOSE_MD = 200;
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
// Service
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
/**
|
|
44
|
+
* Singleton service exposing scan() + apply(). Stateless across calls;
|
|
45
|
+
* the manifest on disk is the persistence layer.
|
|
46
|
+
*/
|
|
47
|
+
export class WikiMigrateService {
|
|
48
|
+
static instance = null;
|
|
49
|
+
static getInstance() {
|
|
50
|
+
if (!this.instance)
|
|
51
|
+
this.instance = new WikiMigrateService();
|
|
52
|
+
return this.instance;
|
|
53
|
+
}
|
|
54
|
+
static resetInstance() {
|
|
55
|
+
this.instance = null;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Dry-run preview of what `apply()` would do.
|
|
59
|
+
*/
|
|
60
|
+
async scan(input) {
|
|
61
|
+
return this.run({ ...input, apply: false });
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Execute the migration: bootstrap vaults, write pages, persist manifest.
|
|
65
|
+
*/
|
|
66
|
+
async apply(input) {
|
|
67
|
+
return this.run({ ...input, apply: true });
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Migrate OSS-distributed SOPs (the ones that ship with Crewly OSS itself)
|
|
71
|
+
* into the GLOBAL vault's `llm-curated/sops/` tree so wiki-query can find
|
|
72
|
+
* them. Without this, the org-wide SOPs (content-production-pipeline,
|
|
73
|
+
* git-workflow, blocker-handling, etc.) sit in `<crewly-src>/config/sops/`
|
|
74
|
+
* + `config/domain-sops/` + `config/templates/pro-sops/` and are invisible
|
|
75
|
+
* to `wiki-query` — which is why ORC reported "no marketing SOP" despite
|
|
76
|
+
* the file existing on disk (2026-05-27 incident).
|
|
77
|
+
*
|
|
78
|
+
* Routing:
|
|
79
|
+
* `config/sops/<role>/<n>.md` → llm-curated/sops/<role>/<n>.md
|
|
80
|
+
* `config/sops/common/<n>.md` → llm-curated/sops/common/<n>.md
|
|
81
|
+
* `config/sops/<n>.md` (no role) → llm-curated/sops/general/<n>.md
|
|
82
|
+
* `config/domain-sops/<n>.sop.md` → llm-curated/sops/domain/<n>.md
|
|
83
|
+
* `config/templates/pro-sops/norms/<n>.md` → llm-curated/sops/pro-norms/<n>.md
|
|
84
|
+
*
|
|
85
|
+
* Idempotent via the global vault's manifest. Legacy source files are NEVER
|
|
86
|
+
* deleted. Bootstraps the global vault if missing.
|
|
87
|
+
*
|
|
88
|
+
* @param input.crewlySourceRoot - Absolute path to the Crewly OSS source
|
|
89
|
+
* directory (the one containing `config/sops/`). Defaults to process.cwd()
|
|
90
|
+
* when the OSS happens to BE running from its own source dir.
|
|
91
|
+
* @param input.apply - false for dry-run, true to write.
|
|
92
|
+
*/
|
|
93
|
+
async migrateOssSops(input) {
|
|
94
|
+
const crewlySrc = input.crewlySourceRoot ?? process.cwd();
|
|
95
|
+
const homeDir = input.homeDir ?? os.homedir();
|
|
96
|
+
if (!path.isAbsolute(crewlySrc)) {
|
|
97
|
+
return {
|
|
98
|
+
ok: false,
|
|
99
|
+
reason: 'invalid_input',
|
|
100
|
+
message: 'crewlySourceRoot must be an absolute path',
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
if (!existsSync(path.join(crewlySrc, 'config'))) {
|
|
104
|
+
return {
|
|
105
|
+
ok: false,
|
|
106
|
+
reason: 'project_root_missing',
|
|
107
|
+
message: `${crewlySrc} doesn't look like Crewly source (no config/ dir)`,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
const globalVault = path.join(homeDir, '.crewly', 'global-wiki');
|
|
111
|
+
const bootstrapNeeded = {
|
|
112
|
+
project: false,
|
|
113
|
+
global: !existsSync(path.join(globalVault, 'SCHEMA.md')),
|
|
114
|
+
teams: [],
|
|
115
|
+
};
|
|
116
|
+
// Manifest lives inside the global vault — reuse the same shape as
|
|
117
|
+
// project migrations so re-runs are idempotent across both flows.
|
|
118
|
+
const manifestPath = path.join(globalVault, WIKI_MIGRATE_MANIFEST_FILENAME);
|
|
119
|
+
const manifest = await this.loadManifest(manifestPath);
|
|
120
|
+
const migratedIds = new Set(manifest.entries.map((e) => e.sourceId));
|
|
121
|
+
const migratedHashes = new Set(manifest.entries.map((e) => e.contentHash));
|
|
122
|
+
const proposed = [];
|
|
123
|
+
const sources = [
|
|
124
|
+
{
|
|
125
|
+
type: 'sop-role',
|
|
126
|
+
relRoot: 'config/sops',
|
|
127
|
+
targetSubdir: (rel) => {
|
|
128
|
+
// rel: e.g. "developer/git-workflow.md" → "developer"
|
|
129
|
+
// or top-level "xhs-requirement-log.v1.md" → "general"
|
|
130
|
+
const parts = rel.split('/');
|
|
131
|
+
return parts.length > 1 ? parts[0] : 'general';
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
type: 'sop-domain',
|
|
136
|
+
relRoot: 'config/domain-sops',
|
|
137
|
+
targetSubdir: () => 'domain',
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
type: 'sop-pro-template',
|
|
141
|
+
relRoot: 'config/templates/pro-sops/norms',
|
|
142
|
+
targetSubdir: () => 'pro-norms',
|
|
143
|
+
},
|
|
144
|
+
];
|
|
145
|
+
for (const src of sources) {
|
|
146
|
+
const absRoot = path.join(crewlySrc, src.relRoot);
|
|
147
|
+
if (!existsSync(absRoot))
|
|
148
|
+
continue;
|
|
149
|
+
await this.collectSopFiles(absRoot, '', async (relPath, absPath) => {
|
|
150
|
+
// Skip EXAMPLE files used for in-source documentation only.
|
|
151
|
+
const lower = relPath.toLowerCase();
|
|
152
|
+
if (lower.includes('example'))
|
|
153
|
+
return;
|
|
154
|
+
let body;
|
|
155
|
+
try {
|
|
156
|
+
body = await fs.readFile(absPath, 'utf8');
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
const sourceId = `${src.type}:${path.posix.join(src.relRoot, relPath)}`;
|
|
162
|
+
const subdir = src.targetSubdir(relPath.replace(/\\/g, '/'));
|
|
163
|
+
const slugBase = path
|
|
164
|
+
.basename(relPath)
|
|
165
|
+
.replace(/\.sop\.md$/i, '')
|
|
166
|
+
.replace(/\.md$/i, '');
|
|
167
|
+
const targetRel = `llm-curated/sops/${subdir}/${slugBase}.md`;
|
|
168
|
+
const titleFromBody = body.match(/^#\s+(.+)$/m)?.[1]?.trim() ?? slugBase;
|
|
169
|
+
const enriched = `${frontmatterForSop(src.type, relPath, src.relRoot, titleFromBody)}${body.replace(/^#\s+.+\n/, '')}`;
|
|
170
|
+
const hash = sha256(enriched);
|
|
171
|
+
const skipReason = migratedIds.has(sourceId) || migratedHashes.has(hash) ? 'already migrated' : undefined;
|
|
172
|
+
proposed.push({
|
|
173
|
+
sourceType: src.type,
|
|
174
|
+
sourceFile: path.posix.join(src.relRoot, relPath.replace(/\\/g, '/')),
|
|
175
|
+
sourceId,
|
|
176
|
+
contentHash: hash,
|
|
177
|
+
targetVaultPath: globalVault,
|
|
178
|
+
targetRelativePath: targetRel,
|
|
179
|
+
title: titleFromBody.slice(0, 100),
|
|
180
|
+
routingUncertain: false,
|
|
181
|
+
});
|
|
182
|
+
if (skipReason)
|
|
183
|
+
proposed[proposed.length - 1].skipReason = skipReason;
|
|
184
|
+
// Cache the rendered body so apply() doesn't have to re-render.
|
|
185
|
+
sopRenderCache.set(sourceId, enriched);
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
const summary = this.summarize(proposed);
|
|
189
|
+
const scan = {
|
|
190
|
+
ok: true,
|
|
191
|
+
vaultPath: globalVault,
|
|
192
|
+
legacyDetected: proposed.length > 0,
|
|
193
|
+
proposedPages: proposed,
|
|
194
|
+
bootstrapNeeded,
|
|
195
|
+
summary,
|
|
196
|
+
};
|
|
197
|
+
if (!input.apply)
|
|
198
|
+
return scan;
|
|
199
|
+
// ── APPLY phase ────────────────────────────────────────────────────
|
|
200
|
+
const bootstrapped = [];
|
|
201
|
+
if (bootstrapNeeded.global) {
|
|
202
|
+
await this.writeGlobalVaultSkeleton(globalVault);
|
|
203
|
+
bootstrapped.push(globalVault);
|
|
204
|
+
}
|
|
205
|
+
let applied = 0;
|
|
206
|
+
let skipped = 0;
|
|
207
|
+
for (const page of proposed) {
|
|
208
|
+
if (page.skipReason) {
|
|
209
|
+
skipped++;
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
const body = sopRenderCache.get(page.sourceId);
|
|
213
|
+
if (!body) {
|
|
214
|
+
page.skipReason = 'render_cache_miss';
|
|
215
|
+
skipped++;
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
try {
|
|
219
|
+
const abs = path.join(page.targetVaultPath, page.targetRelativePath);
|
|
220
|
+
await fs.mkdir(path.dirname(abs), { recursive: true });
|
|
221
|
+
await fs.writeFile(abs, body, 'utf8');
|
|
222
|
+
manifest.entries.push({
|
|
223
|
+
sourceId: page.sourceId,
|
|
224
|
+
contentHash: page.contentHash,
|
|
225
|
+
targetRelativePath: page.targetRelativePath,
|
|
226
|
+
migratedAt: new Date().toISOString(),
|
|
227
|
+
});
|
|
228
|
+
applied++;
|
|
229
|
+
}
|
|
230
|
+
catch (err) {
|
|
231
|
+
page.skipReason = `write_failed: ${err.message}`;
|
|
232
|
+
skipped++;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
if (applied > 0) {
|
|
236
|
+
await this.persistManifest(manifestPath, manifest);
|
|
237
|
+
}
|
|
238
|
+
const applyResult = {
|
|
239
|
+
...scan,
|
|
240
|
+
applied,
|
|
241
|
+
skipped,
|
|
242
|
+
bootstrapped,
|
|
243
|
+
manifestPath,
|
|
244
|
+
};
|
|
245
|
+
return applyResult;
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Recursive walk of a SOP source dir, calling `visit` for every `.md` file
|
|
249
|
+
* with its path RELATIVE to `root`.
|
|
250
|
+
*/
|
|
251
|
+
async collectSopFiles(root, relPrefix, visit) {
|
|
252
|
+
let entries;
|
|
253
|
+
try {
|
|
254
|
+
entries = await fs.readdir(path.join(root, relPrefix), { withFileTypes: true });
|
|
255
|
+
}
|
|
256
|
+
catch {
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
for (const entry of entries) {
|
|
260
|
+
const rel = path.posix.join(relPrefix, entry.name);
|
|
261
|
+
const abs = path.join(root, rel);
|
|
262
|
+
if (entry.isDirectory()) {
|
|
263
|
+
await this.collectSopFiles(root, rel, visit);
|
|
264
|
+
}
|
|
265
|
+
else if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
266
|
+
await visit(rel, abs);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Shared implementation. Branches on `input.apply` for the write phase.
|
|
272
|
+
*/
|
|
273
|
+
async run(input) {
|
|
274
|
+
const { projectRoot } = input;
|
|
275
|
+
if (!projectRoot || !path.isAbsolute(projectRoot)) {
|
|
276
|
+
return {
|
|
277
|
+
ok: false,
|
|
278
|
+
reason: 'invalid_input',
|
|
279
|
+
message: 'projectRoot must be an absolute path',
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
if (!existsSync(projectRoot)) {
|
|
283
|
+
return {
|
|
284
|
+
ok: false,
|
|
285
|
+
reason: 'project_root_missing',
|
|
286
|
+
message: `projectRoot does not exist: ${projectRoot}`,
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
const homeDir = input.homeDir ?? os.homedir();
|
|
290
|
+
const includeMemory = input.includeAgentMemory ?? true;
|
|
291
|
+
const vaultPath = path.join(projectRoot, '.crewly', 'wiki');
|
|
292
|
+
// Bootstrap scoping is needed up-front because we may need to write
|
|
293
|
+
// into the project vault (which requires its SCHEMA.md to exist for
|
|
294
|
+
// the OSS reads — and for the lint contract).
|
|
295
|
+
const bootstrapNeeded = await this.detectBootstrapNeeded(projectRoot, homeDir);
|
|
296
|
+
// Manifest (skip when not yet bootstrapped — we'll create it on apply).
|
|
297
|
+
const manifestPath = path.join(vaultPath, WIKI_MIGRATE_MANIFEST_FILENAME);
|
|
298
|
+
const manifest = await this.loadManifest(manifestPath);
|
|
299
|
+
const migratedIds = new Set(manifest.entries.map((e) => e.sourceId));
|
|
300
|
+
const migratedHashes = new Set(manifest.entries.map((e) => e.contentHash));
|
|
301
|
+
// Discover + propose.
|
|
302
|
+
const proposedPages = [];
|
|
303
|
+
let legacyDetected = false;
|
|
304
|
+
const knowledgeDir = path.join(projectRoot, '.crewly', 'knowledge');
|
|
305
|
+
if (existsSync(knowledgeDir)) {
|
|
306
|
+
legacyDetected = true;
|
|
307
|
+
await this.proposeFromJsonArray(knowledgeDir, 'decisions.json', 'decision', vaultPath, proposedPages, migratedIds, migratedHashes, this.decisionToPage.bind(this));
|
|
308
|
+
await this.proposeFromJsonArray(knowledgeDir, 'patterns.json', 'pattern', vaultPath, proposedPages, migratedIds, migratedHashes, this.patternToPage.bind(this));
|
|
309
|
+
await this.proposeFromJsonArray(knowledgeDir, 'gotchas.json', 'gotcha', vaultPath, proposedPages, migratedIds, migratedHashes, this.gotchaToPage.bind(this));
|
|
310
|
+
await this.proposeFromJsonArray(knowledgeDir, 'relationships.json', 'relationship', vaultPath, proposedPages, migratedIds, migratedHashes, this.relationshipToPage.bind(this));
|
|
311
|
+
await this.proposeLearningsLog(knowledgeDir, vaultPath, proposedPages, migratedIds, migratedHashes);
|
|
312
|
+
await this.proposeLooseMarkdown(knowledgeDir, vaultPath, proposedPages, migratedIds, migratedHashes);
|
|
313
|
+
}
|
|
314
|
+
// (2026-05-27) ALSO scan project ROOT for deploy/agent docs that agents
|
|
315
|
+
// reach for first when asked "how do I deploy this". These live OUTSIDE
|
|
316
|
+
// .crewly/ traditionally — `<root>/CLAUDE.md`, `<root>/DEPLOYMENT.md`,
|
|
317
|
+
// `<root>/AGENTS.md`, `<root>/deploy/*.md`, `<root>/docs/deploy*.md`.
|
|
318
|
+
// Without this pass agents can't wiki-query for deploy procedures and
|
|
319
|
+
// fall back to reading raw CLAUDE.md (the 2026-05-27 Closie deploy
|
|
320
|
+
// incident — ORC told Steve "wiki 里没有部署文档" though CLAUDE.md
|
|
321
|
+
// had it).
|
|
322
|
+
await this.proposeProjectRootDocs(projectRoot, vaultPath, proposedPages, migratedIds, migratedHashes);
|
|
323
|
+
// (2026-05-27) ALSO scan <project>/.crewly/docs/*.md — the legacy
|
|
324
|
+
// `query-knowledge` skill's storage. Same project as Crewly itself has
|
|
325
|
+
// 114 such docs (blog drafts, case studies, business-model, cloud-mvp
|
|
326
|
+
// plan, competitive scans). Without this they're invisible to
|
|
327
|
+
// wiki-query and stuck in the deprecated `query-knowledge` path.
|
|
328
|
+
await this.proposeProjectDocs(projectRoot, vaultPath, proposedPages, migratedIds, migratedHashes);
|
|
329
|
+
if (includeMemory) {
|
|
330
|
+
const agentsDir = path.join(homeDir, '.crewly', 'agents');
|
|
331
|
+
if (existsSync(agentsDir)) {
|
|
332
|
+
const found = await this.proposeFromAgentMemory(agentsDir, vaultPath, proposedPages, migratedIds, migratedHashes);
|
|
333
|
+
if (found)
|
|
334
|
+
legacyDetected = true;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
const summary = this.summarize(proposedPages);
|
|
338
|
+
const scan = {
|
|
339
|
+
ok: true,
|
|
340
|
+
vaultPath,
|
|
341
|
+
legacyDetected,
|
|
342
|
+
proposedPages,
|
|
343
|
+
bootstrapNeeded,
|
|
344
|
+
summary,
|
|
345
|
+
};
|
|
346
|
+
if (!input.apply)
|
|
347
|
+
return scan;
|
|
348
|
+
// ── APPLY PHASE ─────────────────────────────────────────────────────
|
|
349
|
+
const bootstrapped = await this.bootstrapMissingVaults(projectRoot, homeDir, bootstrapNeeded);
|
|
350
|
+
let applied = 0;
|
|
351
|
+
let skipped = 0;
|
|
352
|
+
for (const page of proposedPages) {
|
|
353
|
+
if (page.skipReason) {
|
|
354
|
+
skipped++;
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
try {
|
|
358
|
+
const finalRel = await this.writePage(page);
|
|
359
|
+
manifest.entries.push({
|
|
360
|
+
sourceId: page.sourceId,
|
|
361
|
+
contentHash: page.contentHash,
|
|
362
|
+
targetRelativePath: finalRel,
|
|
363
|
+
migratedAt: new Date().toISOString(),
|
|
364
|
+
});
|
|
365
|
+
applied++;
|
|
366
|
+
}
|
|
367
|
+
catch (err) {
|
|
368
|
+
page.skipReason = `write_failed: ${err.message}`;
|
|
369
|
+
skipped++;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
if (applied > 0) {
|
|
373
|
+
await this.persistManifest(manifestPath, manifest);
|
|
374
|
+
}
|
|
375
|
+
const applyResult = {
|
|
376
|
+
...scan,
|
|
377
|
+
applied,
|
|
378
|
+
skipped,
|
|
379
|
+
bootstrapped,
|
|
380
|
+
manifestPath,
|
|
381
|
+
};
|
|
382
|
+
return applyResult;
|
|
383
|
+
}
|
|
384
|
+
// ---------------------------------------------------------------------------
|
|
385
|
+
// Bootstrap detection + execution
|
|
386
|
+
// ---------------------------------------------------------------------------
|
|
387
|
+
async detectBootstrapNeeded(projectRoot, homeDir) {
|
|
388
|
+
const projectVault = path.join(projectRoot, '.crewly', 'wiki', 'SCHEMA.md');
|
|
389
|
+
const globalVault = path.join(homeDir, '.crewly', 'global-wiki', 'SCHEMA.md');
|
|
390
|
+
const teams = [];
|
|
391
|
+
const teamsRoot = path.join(homeDir, '.crewly', 'teams');
|
|
392
|
+
if (existsSync(teamsRoot)) {
|
|
393
|
+
try {
|
|
394
|
+
const entries = await fs.readdir(teamsRoot, { withFileTypes: true });
|
|
395
|
+
for (const entry of entries) {
|
|
396
|
+
if (!entry.isDirectory())
|
|
397
|
+
continue;
|
|
398
|
+
if (entry.name === 'orchestrator')
|
|
399
|
+
continue;
|
|
400
|
+
const schemaPath = path.join(teamsRoot, entry.name, 'wiki', 'SCHEMA.md');
|
|
401
|
+
if (!existsSync(schemaPath))
|
|
402
|
+
teams.push(entry.name);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
catch {
|
|
406
|
+
// partial discovery is fine
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
return {
|
|
410
|
+
project: !existsSync(projectVault),
|
|
411
|
+
global: !existsSync(globalVault),
|
|
412
|
+
teams,
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
async bootstrapMissingVaults(projectRoot, homeDir, needed) {
|
|
416
|
+
const created = [];
|
|
417
|
+
if (needed.project) {
|
|
418
|
+
const dir = path.join(projectRoot, '.crewly', 'wiki');
|
|
419
|
+
await this.writeProjectVaultSkeleton(dir);
|
|
420
|
+
created.push(dir);
|
|
421
|
+
}
|
|
422
|
+
if (needed.global) {
|
|
423
|
+
const dir = path.join(homeDir, '.crewly', 'global-wiki');
|
|
424
|
+
await this.writeGlobalVaultSkeleton(dir);
|
|
425
|
+
created.push(dir);
|
|
426
|
+
}
|
|
427
|
+
for (const teamUuid of needed.teams) {
|
|
428
|
+
const dir = path.join(homeDir, '.crewly', 'teams', teamUuid, 'wiki');
|
|
429
|
+
let teamName = teamUuid;
|
|
430
|
+
try {
|
|
431
|
+
const cfgRaw = await fs.readFile(path.join(homeDir, '.crewly', 'teams', teamUuid, 'config.json'), 'utf8');
|
|
432
|
+
const cfg = JSON.parse(cfgRaw);
|
|
433
|
+
teamName = cfg.name ?? cfg.teamName ?? teamUuid;
|
|
434
|
+
}
|
|
435
|
+
catch {
|
|
436
|
+
// config.json missing — use UUID
|
|
437
|
+
}
|
|
438
|
+
await this.writeTeamVaultSkeleton(dir, teamUuid, teamName);
|
|
439
|
+
created.push(dir);
|
|
440
|
+
}
|
|
441
|
+
return created;
|
|
442
|
+
}
|
|
443
|
+
async writeProjectVaultSkeleton(vaultDir) {
|
|
444
|
+
await fs.mkdir(path.join(vaultDir, 'memory'), { recursive: true });
|
|
445
|
+
await fs.mkdir(path.join(vaultDir, 'sop-overrides'), { recursive: true });
|
|
446
|
+
await fs.mkdir(path.join(vaultDir, 'llm-curated'), { recursive: true });
|
|
447
|
+
await fs.writeFile(path.join(vaultDir, 'SCHEMA.md'), PROJECT_VAULT_SCHEMA_MD, 'utf8');
|
|
448
|
+
}
|
|
449
|
+
async writeGlobalVaultSkeleton(vaultDir) {
|
|
450
|
+
await fs.mkdir(path.join(vaultDir, 'okr'), { recursive: true });
|
|
451
|
+
await fs.mkdir(path.join(vaultDir, 'decisions'), { recursive: true });
|
|
452
|
+
await fs.mkdir(path.join(vaultDir, 'llm-curated'), { recursive: true });
|
|
453
|
+
await fs.writeFile(path.join(vaultDir, 'SCHEMA.md'), GLOBAL_VAULT_SCHEMA_MD, 'utf8');
|
|
454
|
+
}
|
|
455
|
+
async writeTeamVaultSkeleton(vaultDir, teamUuid, teamName) {
|
|
456
|
+
await fs.mkdir(path.join(vaultDir, 'sop'), { recursive: true });
|
|
457
|
+
await fs.mkdir(path.join(vaultDir, 'team-norm'), { recursive: true });
|
|
458
|
+
await fs.mkdir(path.join(vaultDir, 'llm-curated'), { recursive: true });
|
|
459
|
+
const schema = teamVaultSchemaMd(teamUuid, teamName);
|
|
460
|
+
await fs.writeFile(path.join(vaultDir, 'SCHEMA.md'), schema, 'utf8');
|
|
461
|
+
}
|
|
462
|
+
// ---------------------------------------------------------------------------
|
|
463
|
+
// Manifest persistence
|
|
464
|
+
// ---------------------------------------------------------------------------
|
|
465
|
+
async loadManifest(manifestPath) {
|
|
466
|
+
if (!existsSync(manifestPath)) {
|
|
467
|
+
return { version: WIKI_MIGRATE_MANIFEST_VERSION, entries: [] };
|
|
468
|
+
}
|
|
469
|
+
try {
|
|
470
|
+
const raw = await fs.readFile(manifestPath, 'utf8');
|
|
471
|
+
const parsed = JSON.parse(raw);
|
|
472
|
+
if (!parsed || typeof parsed !== 'object') {
|
|
473
|
+
return { version: WIKI_MIGRATE_MANIFEST_VERSION, entries: [] };
|
|
474
|
+
}
|
|
475
|
+
return {
|
|
476
|
+
version: parsed.version ?? WIKI_MIGRATE_MANIFEST_VERSION,
|
|
477
|
+
entries: Array.isArray(parsed.entries) ? parsed.entries : [],
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
catch {
|
|
481
|
+
// Corrupt manifest — treat as empty. Apply will overwrite.
|
|
482
|
+
return { version: WIKI_MIGRATE_MANIFEST_VERSION, entries: [] };
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
async persistManifest(manifestPath, manifest) {
|
|
486
|
+
await fs.mkdir(path.dirname(manifestPath), { recursive: true });
|
|
487
|
+
const payload = JSON.stringify({ ...manifest, version: WIKI_MIGRATE_MANIFEST_VERSION }, null, 2);
|
|
488
|
+
await fs.writeFile(manifestPath, payload, 'utf8');
|
|
489
|
+
}
|
|
490
|
+
// ---------------------------------------------------------------------------
|
|
491
|
+
// Proposers — one per legacy source type
|
|
492
|
+
// ---------------------------------------------------------------------------
|
|
493
|
+
async readJsonArray(dir, filename) {
|
|
494
|
+
const file = path.join(dir, filename);
|
|
495
|
+
if (!existsSync(file))
|
|
496
|
+
return null;
|
|
497
|
+
try {
|
|
498
|
+
const raw = await fs.readFile(file, 'utf8');
|
|
499
|
+
const parsed = JSON.parse(raw);
|
|
500
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
501
|
+
}
|
|
502
|
+
catch {
|
|
503
|
+
return [];
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
async proposeFromJsonArray(knowledgeDir, filename, type, vaultPath, out, migratedIds, migratedHashes, convert) {
|
|
507
|
+
const items = await this.readJsonArray(knowledgeDir, filename);
|
|
508
|
+
if (items === null)
|
|
509
|
+
return;
|
|
510
|
+
for (const entry of items) {
|
|
511
|
+
const converted = convert(entry, knowledgeDir, filename, vaultPath);
|
|
512
|
+
const hash = sha256(converted.body);
|
|
513
|
+
const skipReason = migratedIds.has(converted.sourceId) || migratedHashes.has(hash)
|
|
514
|
+
? 'already migrated'
|
|
515
|
+
: undefined;
|
|
516
|
+
out.push({
|
|
517
|
+
sourceType: type,
|
|
518
|
+
sourceFile: path.join('.crewly/knowledge', filename),
|
|
519
|
+
sourceId: converted.sourceId,
|
|
520
|
+
contentHash: hash,
|
|
521
|
+
targetVaultPath: vaultPath,
|
|
522
|
+
targetRelativePath: converted.targetRelativePath,
|
|
523
|
+
title: converted.title,
|
|
524
|
+
routingUncertain: converted.routingUncertain,
|
|
525
|
+
skipReason,
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
decisionToPage(entry, _dir, _file, _vault) {
|
|
530
|
+
const id = entry.id ?? sha256(JSON.stringify(entry));
|
|
531
|
+
const date = isoDateOnly(entry.decidedAt) ?? UNDATED_PREFIX;
|
|
532
|
+
const title = entry.title?.trim() && entry.title.trim() !== 'Untitled Decision'
|
|
533
|
+
? entry.title.trim()
|
|
534
|
+
: firstSentence(entry.decision ?? 'untitled decision');
|
|
535
|
+
const slug = makeSlug(title);
|
|
536
|
+
const body = renderDecisionPage(entry, title, id);
|
|
537
|
+
return {
|
|
538
|
+
sourceId: id,
|
|
539
|
+
targetRelativePath: `llm-curated/decisions/${date}-${slug}.md`,
|
|
540
|
+
title,
|
|
541
|
+
routingUncertain: looksCrossProject(entry.decision ?? '', entry.rationale ?? ''),
|
|
542
|
+
body,
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
patternToPage(entry, _dir, _file, _vault) {
|
|
546
|
+
const id = entry.id ?? sha256(JSON.stringify(entry));
|
|
547
|
+
const date = isoDateOnly(entry.createdAt) ?? UNDATED_PREFIX;
|
|
548
|
+
const title = entry.title?.trim() && entry.title.trim() !== 'Untitled Pattern'
|
|
549
|
+
? entry.title.trim()
|
|
550
|
+
: firstSentence(entry.description ?? 'untitled pattern');
|
|
551
|
+
const slug = makeSlug(title);
|
|
552
|
+
const body = renderPatternPage(entry, title, id);
|
|
553
|
+
return {
|
|
554
|
+
sourceId: id,
|
|
555
|
+
targetRelativePath: `llm-curated/patterns/${date}-${slug}.md`,
|
|
556
|
+
title,
|
|
557
|
+
routingUncertain: looksCrossProject(entry.description ?? '', entry.category ?? ''),
|
|
558
|
+
body,
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
gotchaToPage(entry, _dir, _file, _vault) {
|
|
562
|
+
const id = entry.id ?? sha256(JSON.stringify(entry));
|
|
563
|
+
const date = isoDateOnly(entry.createdAt) ?? UNDATED_PREFIX;
|
|
564
|
+
const title = entry.title?.trim() && entry.title.trim() !== 'Gotcha'
|
|
565
|
+
? entry.title.trim()
|
|
566
|
+
: firstSentence(entry.problem ?? 'untitled gotcha');
|
|
567
|
+
const slug = makeSlug(title);
|
|
568
|
+
const body = renderGotchaPage(entry, title, id);
|
|
569
|
+
return {
|
|
570
|
+
sourceId: id,
|
|
571
|
+
targetRelativePath: `llm-curated/patterns/${date}-${slug}.md`,
|
|
572
|
+
title,
|
|
573
|
+
routingUncertain: looksCrossProject(entry.problem ?? '', entry.solution ?? ''),
|
|
574
|
+
body,
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
relationshipToPage(entry, _dir, _file, _vault) {
|
|
578
|
+
const id = entry.id ?? sha256(JSON.stringify(entry));
|
|
579
|
+
const date = isoDateOnly(entry.createdAt) ?? UNDATED_PREFIX;
|
|
580
|
+
const title = entry.title?.trim() || entry.subject?.trim() || 'untitled relationship';
|
|
581
|
+
const slug = makeSlug(title);
|
|
582
|
+
const body = renderRelationshipPage(entry, title, id);
|
|
583
|
+
return {
|
|
584
|
+
sourceId: id,
|
|
585
|
+
targetRelativePath: `llm-curated/people/${date}-${slug}.md`,
|
|
586
|
+
title,
|
|
587
|
+
routingUncertain: false,
|
|
588
|
+
body,
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
async proposeLearningsLog(knowledgeDir, vaultPath, out, migratedIds, migratedHashes) {
|
|
592
|
+
const file = path.join(knowledgeDir, 'learnings.md');
|
|
593
|
+
if (!existsSync(file))
|
|
594
|
+
return;
|
|
595
|
+
let raw = '';
|
|
596
|
+
try {
|
|
597
|
+
raw = await fs.readFile(file, 'utf8');
|
|
598
|
+
}
|
|
599
|
+
catch {
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
if (!raw.trim())
|
|
603
|
+
return;
|
|
604
|
+
const sourceId = `learnings:${sha256(raw).slice(0, 16)}`;
|
|
605
|
+
const body = `\n\n## Migrated learnings.md — imported ${new Date().toISOString()}\n\n${raw.trim()}\n`;
|
|
606
|
+
const hash = sha256(body);
|
|
607
|
+
const skipReason = migratedIds.has(sourceId) || migratedHashes.has(hash) ? 'already migrated' : undefined;
|
|
608
|
+
out.push({
|
|
609
|
+
sourceType: 'learning-log',
|
|
610
|
+
sourceFile: '.crewly/knowledge/learnings.md',
|
|
611
|
+
sourceId,
|
|
612
|
+
contentHash: hash,
|
|
613
|
+
targetVaultPath: vaultPath,
|
|
614
|
+
targetRelativePath: 'llm-curated/log.md',
|
|
615
|
+
title: 'learnings.md (legacy import)',
|
|
616
|
+
routingUncertain: false,
|
|
617
|
+
skipReason,
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
async proposeLooseMarkdown(knowledgeDir, vaultPath, out, migratedIds, migratedHashes) {
|
|
621
|
+
let entries;
|
|
622
|
+
try {
|
|
623
|
+
entries = await fs.readdir(knowledgeDir, { withFileTypes: true });
|
|
624
|
+
}
|
|
625
|
+
catch {
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
let count = 0;
|
|
629
|
+
for (const entry of entries) {
|
|
630
|
+
if (count >= WIKI_MIGRATE_MAX_LOOSE_MD)
|
|
631
|
+
break;
|
|
632
|
+
if (!entry.isFile())
|
|
633
|
+
continue;
|
|
634
|
+
if (!entry.name.endsWith('.md'))
|
|
635
|
+
continue;
|
|
636
|
+
if (entry.name.toLowerCase() === 'learnings.md')
|
|
637
|
+
continue;
|
|
638
|
+
count++;
|
|
639
|
+
const absPath = path.join(knowledgeDir, entry.name);
|
|
640
|
+
let raw;
|
|
641
|
+
try {
|
|
642
|
+
raw = await fs.readFile(absPath, 'utf8');
|
|
643
|
+
}
|
|
644
|
+
catch {
|
|
645
|
+
continue;
|
|
646
|
+
}
|
|
647
|
+
const sourceId = `loose:${entry.name}:${sha256(raw).slice(0, 12)}`;
|
|
648
|
+
const slug = makeSlug(entry.name.replace(/\.md$/i, ''));
|
|
649
|
+
const body = renderLooseMdPage(entry.name, raw);
|
|
650
|
+
const hash = sha256(body);
|
|
651
|
+
const skipReason = migratedIds.has(sourceId) || migratedHashes.has(hash) ? 'already migrated' : undefined;
|
|
652
|
+
out.push({
|
|
653
|
+
sourceType: 'loose-md',
|
|
654
|
+
sourceFile: path.join('.crewly/knowledge', entry.name),
|
|
655
|
+
sourceId,
|
|
656
|
+
contentHash: hash,
|
|
657
|
+
targetVaultPath: vaultPath,
|
|
658
|
+
targetRelativePath: `llm-curated/decisions/${slug}.md`,
|
|
659
|
+
title: entry.name.replace(/\.md$/i, ''),
|
|
660
|
+
routingUncertain: looksCrossProject(raw, ''),
|
|
661
|
+
skipReason,
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
/**
|
|
666
|
+
* (NEW 2026-05-27) Scan the project root for deploy/agent docs that
|
|
667
|
+
* agents would otherwise miss. Each project keeps "how do I deploy"
|
|
668
|
+
* knowledge OUTSIDE `.crewly/` in well-known locations:
|
|
669
|
+
*
|
|
670
|
+
* `<root>/CLAUDE.md` → llm-curated/runbooks/claude-md.md
|
|
671
|
+
* `<root>/AGENTS.md` → llm-curated/runbooks/agents-md.md
|
|
672
|
+
* `<root>/DEPLOYMENT.md` → llm-curated/runbooks/deployment.md
|
|
673
|
+
* `<root>/DEPLOYMENT-GUIDE.md` → llm-curated/runbooks/deployment-guide.md
|
|
674
|
+
* `<root>/deploy/*.md` → llm-curated/runbooks/deploy-<n>.md
|
|
675
|
+
* `<root>/docs/deploy*.md` → llm-curated/runbooks/docs-<n>.md
|
|
676
|
+
* `<root>/.claude/commands/deploy*.md` → llm-curated/runbooks/claude-cmd-<n>.md
|
|
677
|
+
*
|
|
678
|
+
* Frontmatter sets `kind: runbook` so wiki-query / lint can distinguish
|
|
679
|
+
* these from llm-curated/decisions/.
|
|
680
|
+
*/
|
|
681
|
+
async proposeProjectRootDocs(projectRoot, vaultPath, out, migratedIds, migratedHashes) {
|
|
682
|
+
const candidates = [];
|
|
683
|
+
// 1) Well-known top-level files.
|
|
684
|
+
const topLevelNames = [
|
|
685
|
+
'CLAUDE.md',
|
|
686
|
+
'AGENTS.md',
|
|
687
|
+
'DEPLOYMENT.md',
|
|
688
|
+
'DEPLOYMENT-GUIDE.md',
|
|
689
|
+
'DEPLOY.md',
|
|
690
|
+
'RUNBOOK.md',
|
|
691
|
+
];
|
|
692
|
+
for (const name of topLevelNames) {
|
|
693
|
+
if (existsSync(path.join(projectRoot, name))) {
|
|
694
|
+
candidates.push({
|
|
695
|
+
rel: name,
|
|
696
|
+
targetSlug: name.toLowerCase().replace(/\.md$/, ''),
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
// 2) <root>/deploy/*.md
|
|
701
|
+
const deployDir = path.join(projectRoot, 'deploy');
|
|
702
|
+
if (existsSync(deployDir)) {
|
|
703
|
+
try {
|
|
704
|
+
const entries = await fs.readdir(deployDir, { withFileTypes: true });
|
|
705
|
+
for (const e of entries) {
|
|
706
|
+
if (e.isFile() && e.name.endsWith('.md')) {
|
|
707
|
+
candidates.push({
|
|
708
|
+
rel: `deploy/${e.name}`,
|
|
709
|
+
targetSlug: `deploy-${makeSlug(e.name.replace(/\.md$/, ''))}`,
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
catch {
|
|
715
|
+
// ignore
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
// 3) <root>/docs/deploy*.md / runbook*.md
|
|
719
|
+
const docsDir = path.join(projectRoot, 'docs');
|
|
720
|
+
if (existsSync(docsDir)) {
|
|
721
|
+
try {
|
|
722
|
+
const entries = await fs.readdir(docsDir, { withFileTypes: true });
|
|
723
|
+
for (const e of entries) {
|
|
724
|
+
if (!e.isFile() || !e.name.endsWith('.md'))
|
|
725
|
+
continue;
|
|
726
|
+
if (!/^(deploy|runbook)/i.test(e.name))
|
|
727
|
+
continue;
|
|
728
|
+
candidates.push({
|
|
729
|
+
rel: `docs/${e.name}`,
|
|
730
|
+
targetSlug: `docs-${makeSlug(e.name.replace(/\.md$/, ''))}`,
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
catch {
|
|
735
|
+
// ignore
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
// 4) <root>/.claude/commands/deploy*.md (Claude Code custom commands)
|
|
739
|
+
const claudeCmdDir = path.join(projectRoot, '.claude', 'commands');
|
|
740
|
+
if (existsSync(claudeCmdDir)) {
|
|
741
|
+
try {
|
|
742
|
+
const entries = await fs.readdir(claudeCmdDir, { withFileTypes: true });
|
|
743
|
+
for (const e of entries) {
|
|
744
|
+
if (!e.isFile() || !e.name.endsWith('.md'))
|
|
745
|
+
continue;
|
|
746
|
+
if (!/deploy|runbook|release/i.test(e.name))
|
|
747
|
+
continue;
|
|
748
|
+
candidates.push({
|
|
749
|
+
rel: `.claude/commands/${e.name}`,
|
|
750
|
+
targetSlug: `claude-cmd-${makeSlug(e.name.replace(/\.md$/, ''))}`,
|
|
751
|
+
});
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
catch {
|
|
755
|
+
// ignore
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
for (const c of candidates) {
|
|
759
|
+
const absPath = path.join(projectRoot, c.rel);
|
|
760
|
+
let raw;
|
|
761
|
+
try {
|
|
762
|
+
raw = await fs.readFile(absPath, 'utf8');
|
|
763
|
+
}
|
|
764
|
+
catch {
|
|
765
|
+
continue;
|
|
766
|
+
}
|
|
767
|
+
// Skip empty / nearly-empty files (e.g. fresh AGENTS.md placeholders).
|
|
768
|
+
if (raw.trim().length < 40)
|
|
769
|
+
continue;
|
|
770
|
+
const sourceId = `runbook:${c.rel}:${sha256(raw).slice(0, 12)}`;
|
|
771
|
+
const title = raw.match(/^#\s+(.+)$/m)?.[1]?.trim() ?? c.rel.replace(/\.md$/, '');
|
|
772
|
+
const body = frontmatter({
|
|
773
|
+
title: title.slice(0, 120),
|
|
774
|
+
kind: 'runbook',
|
|
775
|
+
migrated_from: c.rel,
|
|
776
|
+
}) + raw.replace(/^#\s+.+\n/, '');
|
|
777
|
+
const hash = sha256(body);
|
|
778
|
+
const skipReason = migratedIds.has(sourceId) || migratedHashes.has(hash) ? 'already migrated' : undefined;
|
|
779
|
+
out.push({
|
|
780
|
+
sourceType: 'loose-md',
|
|
781
|
+
sourceFile: c.rel,
|
|
782
|
+
sourceId,
|
|
783
|
+
contentHash: hash,
|
|
784
|
+
targetVaultPath: vaultPath,
|
|
785
|
+
targetRelativePath: `llm-curated/runbooks/${c.targetSlug}.md`,
|
|
786
|
+
title: title.slice(0, 100),
|
|
787
|
+
routingUncertain: false,
|
|
788
|
+
skipReason,
|
|
789
|
+
});
|
|
790
|
+
// Cache rendered body so writePage doesn't need to re-render.
|
|
791
|
+
runbookRenderCache.set(sourceId, body);
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
/**
|
|
795
|
+
* (NEW 2026-05-27) Scan `<project>/.crewly/docs/*.md` — the storage of
|
|
796
|
+
* the deprecated `query-knowledge` skill. Each file → its own page under
|
|
797
|
+
* `llm-curated/docs/<slug>.md`. Lets wiki-query (v2.1) reach the same
|
|
798
|
+
* content that `query-knowledge` (legacy) used to.
|
|
799
|
+
*
|
|
800
|
+
* Capped at WIKI_MIGRATE_MAX_LOOSE_MD to keep scan bounded.
|
|
801
|
+
*/
|
|
802
|
+
async proposeProjectDocs(projectRoot, vaultPath, out, migratedIds, migratedHashes) {
|
|
803
|
+
const docsDir = path.join(projectRoot, '.crewly', 'docs');
|
|
804
|
+
if (!existsSync(docsDir))
|
|
805
|
+
return;
|
|
806
|
+
let entries;
|
|
807
|
+
try {
|
|
808
|
+
entries = await fs.readdir(docsDir, { withFileTypes: true });
|
|
809
|
+
}
|
|
810
|
+
catch {
|
|
811
|
+
return;
|
|
812
|
+
}
|
|
813
|
+
let count = 0;
|
|
814
|
+
for (const entry of entries) {
|
|
815
|
+
if (count >= WIKI_MIGRATE_MAX_LOOSE_MD)
|
|
816
|
+
break;
|
|
817
|
+
if (!entry.isFile() || !entry.name.endsWith('.md'))
|
|
818
|
+
continue;
|
|
819
|
+
count++;
|
|
820
|
+
const abs = path.join(docsDir, entry.name);
|
|
821
|
+
let raw;
|
|
822
|
+
try {
|
|
823
|
+
raw = await fs.readFile(abs, 'utf8');
|
|
824
|
+
}
|
|
825
|
+
catch {
|
|
826
|
+
continue;
|
|
827
|
+
}
|
|
828
|
+
if (raw.trim().length < 40)
|
|
829
|
+
continue;
|
|
830
|
+
const sourceId = `proj-doc:${entry.name}:${sha256(raw).slice(0, 12)}`;
|
|
831
|
+
const slug = makeSlug(entry.name.replace(/\.md$/i, ''));
|
|
832
|
+
const title = raw.match(/^#\s+(.+)$/m)?.[1]?.trim() ?? entry.name.replace(/\.md$/, '');
|
|
833
|
+
const body = frontmatter({
|
|
834
|
+
title: title.slice(0, 120),
|
|
835
|
+
kind: 'project-doc',
|
|
836
|
+
migrated_from: `.crewly/docs/${entry.name}`,
|
|
837
|
+
}) + raw.replace(/^#\s+.+\n/, '');
|
|
838
|
+
const hash = sha256(body);
|
|
839
|
+
const skipReason = migratedIds.has(sourceId) || migratedHashes.has(hash) ? 'already migrated' : undefined;
|
|
840
|
+
out.push({
|
|
841
|
+
sourceType: 'loose-md',
|
|
842
|
+
sourceFile: path.join('.crewly/docs', entry.name),
|
|
843
|
+
sourceId,
|
|
844
|
+
contentHash: hash,
|
|
845
|
+
targetVaultPath: vaultPath,
|
|
846
|
+
targetRelativePath: `llm-curated/docs/${slug}.md`,
|
|
847
|
+
title: title.slice(0, 100),
|
|
848
|
+
routingUncertain: false,
|
|
849
|
+
skipReason,
|
|
850
|
+
});
|
|
851
|
+
runbookRenderCache.set(sourceId, body);
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
async proposeFromAgentMemory(agentsDir, vaultPath, out, migratedIds, migratedHashes) {
|
|
855
|
+
let agentDirs;
|
|
856
|
+
try {
|
|
857
|
+
agentDirs = await fs.readdir(agentsDir, { withFileTypes: true });
|
|
858
|
+
}
|
|
859
|
+
catch {
|
|
860
|
+
return false;
|
|
861
|
+
}
|
|
862
|
+
let anyFound = false;
|
|
863
|
+
for (const agentEntry of agentDirs) {
|
|
864
|
+
if (!agentEntry.isDirectory())
|
|
865
|
+
continue;
|
|
866
|
+
const memoryFile = path.join(agentsDir, agentEntry.name, 'memory.json');
|
|
867
|
+
if (!existsSync(memoryFile))
|
|
868
|
+
continue;
|
|
869
|
+
let raw;
|
|
870
|
+
try {
|
|
871
|
+
raw = await fs.readFile(memoryFile, 'utf8');
|
|
872
|
+
}
|
|
873
|
+
catch {
|
|
874
|
+
continue;
|
|
875
|
+
}
|
|
876
|
+
let parsed;
|
|
877
|
+
try {
|
|
878
|
+
parsed = JSON.parse(raw);
|
|
879
|
+
}
|
|
880
|
+
catch {
|
|
881
|
+
continue;
|
|
882
|
+
}
|
|
883
|
+
if (!parsed.roleKnowledge?.length)
|
|
884
|
+
continue;
|
|
885
|
+
anyFound = true;
|
|
886
|
+
for (const item of parsed.roleKnowledge) {
|
|
887
|
+
const id = item.id ?? sha256(JSON.stringify(item));
|
|
888
|
+
const date = isoDateOnly(item.createdAt) ?? UNDATED_PREFIX;
|
|
889
|
+
const title = firstSentence(item.content ?? 'untitled memory');
|
|
890
|
+
const slug = makeSlug(title);
|
|
891
|
+
const folder = memoryCategoryToFolder(item.category);
|
|
892
|
+
const targetRel = `llm-curated/${folder}/${date}-${slug}.md`;
|
|
893
|
+
const body = renderMemoryEntryPage(item, agentEntry.name, title, id);
|
|
894
|
+
const hash = sha256(body);
|
|
895
|
+
const skipReason = migratedIds.has(id) || migratedHashes.has(hash) ? 'already migrated' : undefined;
|
|
896
|
+
out.push({
|
|
897
|
+
sourceType: 'memory-entry',
|
|
898
|
+
sourceFile: path.join('~/.crewly/agents', agentEntry.name, 'memory.json'),
|
|
899
|
+
sourceId: id,
|
|
900
|
+
contentHash: hash,
|
|
901
|
+
targetVaultPath: vaultPath,
|
|
902
|
+
targetRelativePath: targetRel,
|
|
903
|
+
title,
|
|
904
|
+
routingUncertain: looksCrossProject(item.content ?? '', ''),
|
|
905
|
+
skipReason,
|
|
906
|
+
});
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
return anyFound;
|
|
910
|
+
}
|
|
911
|
+
// ---------------------------------------------------------------------------
|
|
912
|
+
// Write phase
|
|
913
|
+
// ---------------------------------------------------------------------------
|
|
914
|
+
/**
|
|
915
|
+
* Write the proposed page's body to disk under the target vault.
|
|
916
|
+
* Returns the FINAL relativePath written (with a suffix if a collision
|
|
917
|
+
* forced one).
|
|
918
|
+
*/
|
|
919
|
+
async writePage(page) {
|
|
920
|
+
const body = await this.renderForPage(page);
|
|
921
|
+
// Special case: log.md — APPEND rather than overwrite/collide. The
|
|
922
|
+
// log is shared across all imports and the page emitter writes a
|
|
923
|
+
// delimiter header, so concatenation is safe + expected.
|
|
924
|
+
if (page.sourceType === 'learning-log') {
|
|
925
|
+
const abs = path.join(page.targetVaultPath, page.targetRelativePath);
|
|
926
|
+
const existing = existsSync(abs) ? await fs.readFile(abs, 'utf8') : '';
|
|
927
|
+
await fs.mkdir(path.dirname(abs), { recursive: true });
|
|
928
|
+
await fs.writeFile(abs, existing + body, 'utf8');
|
|
929
|
+
return page.targetRelativePath;
|
|
930
|
+
}
|
|
931
|
+
let relativePath = page.targetRelativePath;
|
|
932
|
+
let abs = path.join(page.targetVaultPath, relativePath);
|
|
933
|
+
// If a different file already exists at the target, append a suffix
|
|
934
|
+
// until we find an unused slot. (Skip when content matches — that
|
|
935
|
+
// means a previous run already wrote this exact body.)
|
|
936
|
+
let attempt = 0;
|
|
937
|
+
while (existsSync(abs)) {
|
|
938
|
+
try {
|
|
939
|
+
const existing = await fs.readFile(abs, 'utf8');
|
|
940
|
+
if (existing === body)
|
|
941
|
+
return relativePath;
|
|
942
|
+
}
|
|
943
|
+
catch {
|
|
944
|
+
// unreadable; treat as collision
|
|
945
|
+
}
|
|
946
|
+
attempt++;
|
|
947
|
+
const m = page.targetRelativePath.match(/^(.+)\.md$/);
|
|
948
|
+
const stem = m ? m[1] : page.targetRelativePath;
|
|
949
|
+
relativePath = `${stem}-${attempt}.md`;
|
|
950
|
+
abs = path.join(page.targetVaultPath, relativePath);
|
|
951
|
+
if (attempt > 50) {
|
|
952
|
+
throw new Error('collision_resolution_exhausted');
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
await fs.mkdir(path.dirname(abs), { recursive: true });
|
|
956
|
+
await fs.writeFile(abs, body, 'utf8');
|
|
957
|
+
return relativePath;
|
|
958
|
+
}
|
|
959
|
+
/**
|
|
960
|
+
* Re-emit the body for a page. The body is computed once during scan
|
|
961
|
+
* (for content-hash determination) but we don't keep it on the proposal
|
|
962
|
+
* — re-derive from the source on apply. This avoids duplicating large
|
|
963
|
+
* blobs in API responses.
|
|
964
|
+
*/
|
|
965
|
+
async renderForPage(page) {
|
|
966
|
+
// For simplicity we re-read the source and re-render. The hash will
|
|
967
|
+
// re-match because the rendering is deterministic.
|
|
968
|
+
// TODO(perf): cache the rendered bodies on the page object if scans
|
|
969
|
+
// become slow.
|
|
970
|
+
// Cache fast-path: runbook scans (proposeProjectRootDocs) populate
|
|
971
|
+
// runbookRenderCache during scan because their source path lives
|
|
972
|
+
// OUTSIDE .crewly/knowledge/ — the legacy re-read paths below can't
|
|
973
|
+
// find them. SOP migration uses sopRenderCache the same way.
|
|
974
|
+
const cached = runbookRenderCache.get(page.sourceId) ?? sopRenderCache.get(page.sourceId);
|
|
975
|
+
if (cached)
|
|
976
|
+
return cached;
|
|
977
|
+
switch (page.sourceType) {
|
|
978
|
+
case 'decision':
|
|
979
|
+
case 'pattern':
|
|
980
|
+
case 'gotcha':
|
|
981
|
+
case 'relationship': {
|
|
982
|
+
const filename = path.basename(page.sourceFile);
|
|
983
|
+
const knowledgeDir = path.join(page.targetVaultPath.replace(/\.crewly\/wiki$/, ''), '.crewly', 'knowledge');
|
|
984
|
+
const items = await this.readJsonArray(knowledgeDir, filename);
|
|
985
|
+
if (!items)
|
|
986
|
+
throw new Error('source_file_vanished');
|
|
987
|
+
// Match by id when present, else by content hash via re-render.
|
|
988
|
+
for (const entry of items) {
|
|
989
|
+
let convert;
|
|
990
|
+
switch (page.sourceType) {
|
|
991
|
+
case 'decision':
|
|
992
|
+
convert = this.decisionToPage.bind(this);
|
|
993
|
+
break;
|
|
994
|
+
case 'pattern':
|
|
995
|
+
convert = this.patternToPage.bind(this);
|
|
996
|
+
break;
|
|
997
|
+
case 'gotcha':
|
|
998
|
+
convert = this.gotchaToPage.bind(this);
|
|
999
|
+
break;
|
|
1000
|
+
default:
|
|
1001
|
+
convert = this.relationshipToPage.bind(this);
|
|
1002
|
+
break;
|
|
1003
|
+
}
|
|
1004
|
+
const proposed = convert(entry, knowledgeDir, filename, page.targetVaultPath);
|
|
1005
|
+
if (proposed.sourceId === page.sourceId)
|
|
1006
|
+
return proposed.body;
|
|
1007
|
+
}
|
|
1008
|
+
throw new Error('source_entry_vanished');
|
|
1009
|
+
}
|
|
1010
|
+
case 'learning-log': {
|
|
1011
|
+
const knowledgeDir = path.join(page.targetVaultPath.replace(/\.crewly\/wiki$/, ''), '.crewly', 'knowledge');
|
|
1012
|
+
const file = path.join(knowledgeDir, 'learnings.md');
|
|
1013
|
+
const raw = await fs.readFile(file, 'utf8');
|
|
1014
|
+
return `\n\n## Migrated learnings.md — imported ${new Date().toISOString()}\n\n${raw.trim()}\n`;
|
|
1015
|
+
}
|
|
1016
|
+
case 'loose-md': {
|
|
1017
|
+
const knowledgeDir = path.join(page.targetVaultPath.replace(/\.crewly\/wiki$/, ''), '.crewly', 'knowledge');
|
|
1018
|
+
const filename = path.basename(page.sourceFile);
|
|
1019
|
+
const raw = await fs.readFile(path.join(knowledgeDir, filename), 'utf8');
|
|
1020
|
+
return renderLooseMdPage(filename, raw);
|
|
1021
|
+
}
|
|
1022
|
+
case 'memory-entry': {
|
|
1023
|
+
// sourceId is the entry id; re-find by scanning agent dirs.
|
|
1024
|
+
const homeAgentsRoot = page.sourceFile.replace(/~\/\.crewly\/agents\/.*$/, '');
|
|
1025
|
+
const home = homeAgentsRoot.startsWith('~/')
|
|
1026
|
+
? path.join(os.homedir(), '.crewly', 'agents')
|
|
1027
|
+
: path.join(os.homedir(), '.crewly', 'agents');
|
|
1028
|
+
const m = page.sourceFile.match(/agents\/([^/]+)\//);
|
|
1029
|
+
if (!m)
|
|
1030
|
+
throw new Error('memory_source_unparseable');
|
|
1031
|
+
const agentName = m[1];
|
|
1032
|
+
const raw = await fs.readFile(path.join(home, agentName, 'memory.json'), 'utf8');
|
|
1033
|
+
const parsed = JSON.parse(raw);
|
|
1034
|
+
const item = parsed.roleKnowledge?.find((k) => k.id === page.sourceId);
|
|
1035
|
+
if (!item)
|
|
1036
|
+
throw new Error('memory_entry_vanished');
|
|
1037
|
+
const title = firstSentence(item.content ?? 'untitled memory');
|
|
1038
|
+
return renderMemoryEntryPage(item, agentName, title, page.sourceId);
|
|
1039
|
+
}
|
|
1040
|
+
default:
|
|
1041
|
+
throw new Error(`unknown_source_type: ${page.sourceType}`);
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
summarize(pages) {
|
|
1045
|
+
const s = {
|
|
1046
|
+
decisions: 0,
|
|
1047
|
+
patterns: 0,
|
|
1048
|
+
gotchas: 0,
|
|
1049
|
+
relationships: 0,
|
|
1050
|
+
learnings: 0,
|
|
1051
|
+
looseMd: 0,
|
|
1052
|
+
memoryEntries: 0,
|
|
1053
|
+
alreadyMigrated: 0,
|
|
1054
|
+
};
|
|
1055
|
+
for (const p of pages) {
|
|
1056
|
+
if (p.skipReason === 'already migrated')
|
|
1057
|
+
s.alreadyMigrated++;
|
|
1058
|
+
switch (p.sourceType) {
|
|
1059
|
+
case 'decision':
|
|
1060
|
+
s.decisions++;
|
|
1061
|
+
break;
|
|
1062
|
+
case 'pattern':
|
|
1063
|
+
s.patterns++;
|
|
1064
|
+
break;
|
|
1065
|
+
case 'gotcha':
|
|
1066
|
+
s.gotchas++;
|
|
1067
|
+
break;
|
|
1068
|
+
case 'relationship':
|
|
1069
|
+
s.relationships++;
|
|
1070
|
+
break;
|
|
1071
|
+
case 'learning-log':
|
|
1072
|
+
s.learnings++;
|
|
1073
|
+
break;
|
|
1074
|
+
case 'loose-md':
|
|
1075
|
+
s.looseMd++;
|
|
1076
|
+
break;
|
|
1077
|
+
case 'memory-entry':
|
|
1078
|
+
s.memoryEntries++;
|
|
1079
|
+
break;
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
return s;
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
// ---------------------------------------------------------------------------
|
|
1086
|
+
// Renderers — pure functions (test-friendly)
|
|
1087
|
+
// ---------------------------------------------------------------------------
|
|
1088
|
+
const UNDATED_PREFIX = '0000-00-00';
|
|
1089
|
+
/**
|
|
1090
|
+
* In-memory cache of (sopSourceId → rendered body). Populated during scan
|
|
1091
|
+
* and read by apply so we don't re-read source files twice. Keyed by the
|
|
1092
|
+
* stable sourceId so re-running scan+apply within one process doesn't
|
|
1093
|
+
* re-render. Stays in memory only — never persisted.
|
|
1094
|
+
*/
|
|
1095
|
+
const sopRenderCache = new Map();
|
|
1096
|
+
/**
|
|
1097
|
+
* Same cache pattern for runbook / deploy doc imports (proposeProjectRootDocs).
|
|
1098
|
+
* Lets writePage skip the re-read in `renderForPage`.
|
|
1099
|
+
*/
|
|
1100
|
+
const runbookRenderCache = new Map();
|
|
1101
|
+
/**
|
|
1102
|
+
* Frontmatter line for a migrated SOP page. Lets wiki-query / lint
|
|
1103
|
+
* distinguish OSS-distributed SOPs from team-authored ones and lets
|
|
1104
|
+
* future re-imports detect "this came from config/<x>" exactly.
|
|
1105
|
+
*/
|
|
1106
|
+
function frontmatterForSop(type, relPath, relRoot, title) {
|
|
1107
|
+
return frontmatter({
|
|
1108
|
+
title,
|
|
1109
|
+
sop_kind: type,
|
|
1110
|
+
migrated_from: `${relRoot}/${relPath.replace(/\\/g, '/')}`,
|
|
1111
|
+
canonical: true,
|
|
1112
|
+
});
|
|
1113
|
+
}
|
|
1114
|
+
function renderDecisionPage(entry, title, id) {
|
|
1115
|
+
return `${frontmatter({
|
|
1116
|
+
title,
|
|
1117
|
+
migrated_from: '.crewly/knowledge/decisions.json',
|
|
1118
|
+
original_id: id,
|
|
1119
|
+
original_author: entry.decidedBy,
|
|
1120
|
+
original_date: entry.decidedAt,
|
|
1121
|
+
status: entry.status,
|
|
1122
|
+
routing_uncertain: looksCrossProject(entry.decision ?? '', entry.rationale ?? ''),
|
|
1123
|
+
})}# ${escapeHeading(title)}
|
|
1124
|
+
|
|
1125
|
+
${entry.decision ?? '(no decision text)'}
|
|
1126
|
+
|
|
1127
|
+
${entry.rationale ? `\n## Rationale\n\n${entry.rationale}\n` : ''}
|
|
1128
|
+
`;
|
|
1129
|
+
}
|
|
1130
|
+
function renderPatternPage(entry, title, id) {
|
|
1131
|
+
return `${frontmatter({
|
|
1132
|
+
title,
|
|
1133
|
+
migrated_from: '.crewly/knowledge/patterns.json',
|
|
1134
|
+
original_id: id,
|
|
1135
|
+
original_author: entry.discoveredBy,
|
|
1136
|
+
original_date: entry.createdAt,
|
|
1137
|
+
category: entry.category,
|
|
1138
|
+
routing_uncertain: looksCrossProject(entry.description ?? '', entry.category ?? ''),
|
|
1139
|
+
})}# ${escapeHeading(title)}
|
|
1140
|
+
|
|
1141
|
+
${entry.description ?? '(no description)'}
|
|
1142
|
+
`;
|
|
1143
|
+
}
|
|
1144
|
+
function renderGotchaPage(entry, title, id) {
|
|
1145
|
+
return `${frontmatter({
|
|
1146
|
+
title,
|
|
1147
|
+
migrated_from: '.crewly/knowledge/gotchas.json',
|
|
1148
|
+
original_id: id,
|
|
1149
|
+
original_author: entry.discoveredBy,
|
|
1150
|
+
original_date: entry.createdAt,
|
|
1151
|
+
severity: entry.severity,
|
|
1152
|
+
routing_uncertain: looksCrossProject(entry.problem ?? '', entry.solution ?? ''),
|
|
1153
|
+
})}# ${escapeHeading(title)}
|
|
1154
|
+
|
|
1155
|
+
## Problem
|
|
1156
|
+
|
|
1157
|
+
${entry.problem ?? '(no problem text)'}
|
|
1158
|
+
|
|
1159
|
+
${entry.solution ? `## Solution\n\n${entry.solution}\n` : ''}
|
|
1160
|
+
`;
|
|
1161
|
+
}
|
|
1162
|
+
function renderRelationshipPage(entry, title, id) {
|
|
1163
|
+
return `${frontmatter({
|
|
1164
|
+
title,
|
|
1165
|
+
migrated_from: '.crewly/knowledge/relationships.json',
|
|
1166
|
+
original_id: id,
|
|
1167
|
+
original_date: entry.createdAt,
|
|
1168
|
+
})}# ${escapeHeading(title)}
|
|
1169
|
+
|
|
1170
|
+
${entry.description ?? entry.subject ?? '(no description)'}
|
|
1171
|
+
`;
|
|
1172
|
+
}
|
|
1173
|
+
function renderLooseMdPage(filename, raw) {
|
|
1174
|
+
const trimmed = raw.trim();
|
|
1175
|
+
// If the file already starts with a YAML frontmatter block we leave it
|
|
1176
|
+
// intact; otherwise we prepend our own.
|
|
1177
|
+
if (trimmed.startsWith('---'))
|
|
1178
|
+
return raw;
|
|
1179
|
+
return `${frontmatter({
|
|
1180
|
+
title: filename.replace(/\.md$/i, ''),
|
|
1181
|
+
migrated_from: `.crewly/knowledge/${filename}`,
|
|
1182
|
+
})}${raw}`;
|
|
1183
|
+
}
|
|
1184
|
+
function renderMemoryEntryPage(item, agentName, title, id) {
|
|
1185
|
+
return `${frontmatter({
|
|
1186
|
+
title,
|
|
1187
|
+
migrated_from: `agent/${agentName}/memory.json`,
|
|
1188
|
+
original_id: id,
|
|
1189
|
+
original_author: agentName,
|
|
1190
|
+
original_date: item.createdAt,
|
|
1191
|
+
category: item.category,
|
|
1192
|
+
confidence: item.confidence,
|
|
1193
|
+
routing_uncertain: looksCrossProject(item.content ?? '', ''),
|
|
1194
|
+
})}# ${escapeHeading(title)}
|
|
1195
|
+
|
|
1196
|
+
${item.content ?? '(no content)'}
|
|
1197
|
+
`;
|
|
1198
|
+
}
|
|
1199
|
+
// ---------------------------------------------------------------------------
|
|
1200
|
+
// Helpers
|
|
1201
|
+
// ---------------------------------------------------------------------------
|
|
1202
|
+
function sha256(value) {
|
|
1203
|
+
return createHash('sha256').update(value).digest('hex');
|
|
1204
|
+
}
|
|
1205
|
+
function isoDateOnly(value) {
|
|
1206
|
+
if (!value)
|
|
1207
|
+
return null;
|
|
1208
|
+
const d = new Date(value);
|
|
1209
|
+
if (Number.isNaN(d.getTime()))
|
|
1210
|
+
return null;
|
|
1211
|
+
return d.toISOString().slice(0, 10);
|
|
1212
|
+
}
|
|
1213
|
+
function makeSlug(value) {
|
|
1214
|
+
const base = value
|
|
1215
|
+
.toLowerCase()
|
|
1216
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
1217
|
+
.replace(/-+/g, '-')
|
|
1218
|
+
.replace(/^-|-$/g, '');
|
|
1219
|
+
return base.length > 0 ? base.slice(0, 80) : 'untitled';
|
|
1220
|
+
}
|
|
1221
|
+
function firstSentence(value) {
|
|
1222
|
+
const trimmed = value.trim();
|
|
1223
|
+
if (!trimmed)
|
|
1224
|
+
return 'untitled';
|
|
1225
|
+
const cut = trimmed.split(/[.!?\n]/)[0].trim();
|
|
1226
|
+
return (cut.length > 0 ? cut : trimmed).slice(0, 100);
|
|
1227
|
+
}
|
|
1228
|
+
function escapeHeading(value) {
|
|
1229
|
+
return value.replace(/\n/g, ' ');
|
|
1230
|
+
}
|
|
1231
|
+
function memoryCategoryToFolder(category) {
|
|
1232
|
+
switch ((category ?? '').toLowerCase()) {
|
|
1233
|
+
case 'best-practice':
|
|
1234
|
+
case 'pattern':
|
|
1235
|
+
case 'gotcha':
|
|
1236
|
+
case 'lesson':
|
|
1237
|
+
return 'patterns';
|
|
1238
|
+
case 'decision':
|
|
1239
|
+
return 'decisions';
|
|
1240
|
+
case 'person':
|
|
1241
|
+
case 'people':
|
|
1242
|
+
case 'customer':
|
|
1243
|
+
return 'people';
|
|
1244
|
+
default:
|
|
1245
|
+
return 'patterns';
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
/**
|
|
1249
|
+
* Cheap heuristic for "this entry looks like it might span projects."
|
|
1250
|
+
* Returns true when the content references multiple known foreign-scope
|
|
1251
|
+
* markers (team UUIDs, foreign project names, ALL-CAPS pricing or OKR
|
|
1252
|
+
* lingo). Used to flag entries for human review during the next lint.
|
|
1253
|
+
*/
|
|
1254
|
+
function looksCrossProject(...texts) {
|
|
1255
|
+
const joined = texts.join(' ').toLowerCase();
|
|
1256
|
+
if (!joined)
|
|
1257
|
+
return false;
|
|
1258
|
+
const signals = [
|
|
1259
|
+
/\bteam[- ]uuid\b/,
|
|
1260
|
+
/\bokr\b/,
|
|
1261
|
+
/\bcompany[- ]?wide\b/,
|
|
1262
|
+
/\bclosie\b/,
|
|
1263
|
+
/\bglitchy\b/,
|
|
1264
|
+
/\bportal\b.*\bproduct\b/,
|
|
1265
|
+
];
|
|
1266
|
+
return signals.some((re) => re.test(joined));
|
|
1267
|
+
}
|
|
1268
|
+
function frontmatter(fields) {
|
|
1269
|
+
const lines = ['---'];
|
|
1270
|
+
for (const [key, value] of Object.entries(fields)) {
|
|
1271
|
+
if (value === undefined || value === null || value === '')
|
|
1272
|
+
continue;
|
|
1273
|
+
if (typeof value === 'string') {
|
|
1274
|
+
lines.push(`${key}: "${value.replace(/"/g, '\\"').replace(/\n/g, ' ')}"`);
|
|
1275
|
+
}
|
|
1276
|
+
else {
|
|
1277
|
+
lines.push(`${key}: ${value}`);
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
lines.push('---');
|
|
1281
|
+
return lines.join('\n') + '\n\n';
|
|
1282
|
+
}
|
|
1283
|
+
// ---------------------------------------------------------------------------
|
|
1284
|
+
// SCHEMA.md skeletons
|
|
1285
|
+
// ---------------------------------------------------------------------------
|
|
1286
|
+
const PROJECT_VAULT_SCHEMA_MD = `# Project vault — SCHEMA.md
|
|
1287
|
+
# Bootstrapped by wiki-migrate per v2.1 spec §1(C).
|
|
1288
|
+
|
|
1289
|
+
vault_scope: project
|
|
1290
|
+
vault_id: project
|
|
1291
|
+
|
|
1292
|
+
hardcoded:
|
|
1293
|
+
- path: memory/
|
|
1294
|
+
frozen: true
|
|
1295
|
+
description: "Project-scoped facts agents share. Replaces scattered .crewly/knowledge/learnings.md."
|
|
1296
|
+
referenced_by:
|
|
1297
|
+
- skill:remember
|
|
1298
|
+
- skill:recall
|
|
1299
|
+
|
|
1300
|
+
- path: sop-overrides/
|
|
1301
|
+
frozen: true
|
|
1302
|
+
description: "Project-specific SOP deltas. get-sops reads team-vault sop/ first, then this override layer."
|
|
1303
|
+
referenced_by:
|
|
1304
|
+
- skill:get-sops
|
|
1305
|
+
|
|
1306
|
+
llm_curated:
|
|
1307
|
+
- path: llm-curated/
|
|
1308
|
+
frozen: false
|
|
1309
|
+
seed_subdirs:
|
|
1310
|
+
- decisions
|
|
1311
|
+
- patterns
|
|
1312
|
+
- people
|
|
1313
|
+
- log.md
|
|
1314
|
+
- index.md
|
|
1315
|
+
llm_can_create_subdirs: true
|
|
1316
|
+
lint_may_restructure: true
|
|
1317
|
+
|
|
1318
|
+
write_policy:
|
|
1319
|
+
canonical:
|
|
1320
|
+
- team-leader
|
|
1321
|
+
- orchestrator
|
|
1322
|
+
proposed_only:
|
|
1323
|
+
- worker
|
|
1324
|
+
- researcher
|
|
1325
|
+
schema_writer:
|
|
1326
|
+
- steve
|
|
1327
|
+
- architect
|
|
1328
|
+
`;
|
|
1329
|
+
const GLOBAL_VAULT_SCHEMA_MD = `# Global ORC vault — SCHEMA.md
|
|
1330
|
+
# Bootstrapped by wiki-migrate per v2.1 spec §1(B).
|
|
1331
|
+
|
|
1332
|
+
vault_scope: global
|
|
1333
|
+
vault_id: crewly-global
|
|
1334
|
+
|
|
1335
|
+
hardcoded:
|
|
1336
|
+
- path: okr/
|
|
1337
|
+
frozen: true
|
|
1338
|
+
description: "Company OKRs. Authored by Steve; agents read-only."
|
|
1339
|
+
referenced_by:
|
|
1340
|
+
- skill:get-sops
|
|
1341
|
+
|
|
1342
|
+
- path: decisions/
|
|
1343
|
+
frozen: true
|
|
1344
|
+
description: "Cross-project decisions (Anthropic-SMB pricing, etc.). TLs promote project-scoped decisions up here when they apply across teams."
|
|
1345
|
+
referenced_by:
|
|
1346
|
+
- service:sop.service
|
|
1347
|
+
|
|
1348
|
+
llm_curated:
|
|
1349
|
+
- path: llm-curated/
|
|
1350
|
+
frozen: false
|
|
1351
|
+
seed_subdirs:
|
|
1352
|
+
- roadmap
|
|
1353
|
+
- patterns
|
|
1354
|
+
- people
|
|
1355
|
+
- log.md
|
|
1356
|
+
- index.md
|
|
1357
|
+
llm_can_create_subdirs: true
|
|
1358
|
+
lint_may_restructure: true
|
|
1359
|
+
|
|
1360
|
+
write_policy:
|
|
1361
|
+
canonical:
|
|
1362
|
+
- orchestrator
|
|
1363
|
+
proposed_only:
|
|
1364
|
+
- team-leader
|
|
1365
|
+
schema_writer:
|
|
1366
|
+
- steve
|
|
1367
|
+
- architect
|
|
1368
|
+
`;
|
|
1369
|
+
function teamVaultSchemaMd(teamUuid, teamName) {
|
|
1370
|
+
const safeName = teamName.replace(/"/g, '\\"');
|
|
1371
|
+
return `# ${safeName} team vault — SCHEMA.md
|
|
1372
|
+
# Bootstrapped by wiki-migrate per v2.1 spec §1(A).
|
|
1373
|
+
# Team UUID: ${teamUuid}
|
|
1374
|
+
|
|
1375
|
+
vault_scope: team
|
|
1376
|
+
vault_id: ${teamUuid}
|
|
1377
|
+
|
|
1378
|
+
hardcoded:
|
|
1379
|
+
- path: sop/
|
|
1380
|
+
frozen: true
|
|
1381
|
+
description: "Team SOPs. Canonical source for get-sops once migrated; today config/sops/<role>/ remains the fallback."
|
|
1382
|
+
referenced_by:
|
|
1383
|
+
- skill:get-sops
|
|
1384
|
+
- service:sop.service
|
|
1385
|
+
|
|
1386
|
+
- path: team-norm/
|
|
1387
|
+
frozen: true
|
|
1388
|
+
description: "Team norms (canDelegate, role conventions, ROE). Maps from config/sops/<role>/team-norm/ over Phase 2 migration."
|
|
1389
|
+
referenced_by:
|
|
1390
|
+
- skill:get-team-norms
|
|
1391
|
+
|
|
1392
|
+
llm_curated:
|
|
1393
|
+
- path: llm-curated/
|
|
1394
|
+
frozen: false
|
|
1395
|
+
seed_subdirs:
|
|
1396
|
+
- people
|
|
1397
|
+
- decisions
|
|
1398
|
+
- patterns
|
|
1399
|
+
- log.md
|
|
1400
|
+
- index.md
|
|
1401
|
+
llm_can_create_subdirs: true
|
|
1402
|
+
lint_may_restructure: true
|
|
1403
|
+
|
|
1404
|
+
write_policy:
|
|
1405
|
+
canonical:
|
|
1406
|
+
- team-leader
|
|
1407
|
+
- orchestrator
|
|
1408
|
+
proposed_only:
|
|
1409
|
+
- worker
|
|
1410
|
+
- researcher
|
|
1411
|
+
schema_writer:
|
|
1412
|
+
- steve
|
|
1413
|
+
- architect
|
|
1414
|
+
`;
|
|
1415
|
+
}
|
|
1416
|
+
//# sourceMappingURL=wiki-migrate.service.js.map
|