Legacy Workflow Tree Import
Feature Overview [edit]
memo import migrates a legacy workflow tree (docs produced by earlier ws-* harnesses) from a consumer repository into the external vault so product git stays clean. The engine is importWorkflowTree in src/importer.ts; the CLI is memo import [productRoot] [--from <path>] [--cwd <path>] [--vaultRoot <path>] [--json].
The importer maps legacy artifacts into standard vault record kinds (spec, trap, decision, plan, state, log) and is safe to re-run: an unchanged tree becomes a per-record no-op instead of duplicating records.
Business Rules & Logic [edit]
Source discovery (each candidate is only used when it exists):
- Specs:
.agents/specs,specs,.spec-memo/specs. - Memory:
memory,.agents/memory,ws-shared/memory. - Plans:
.agents/plans,plans. - Changelog:
CHANGELOG.md,.agents/CHANGELOG.md,ws-shared/CHANGELOG.md.
Mapping rules:
- Specs (
*.spec.md, or*.mdexcept files whose lowercased name containsindex.prd): slug from frontmatterslug/idelse the filename; title from frontmattertitleelse the first#heading (theSpecification —prefix is stripped) else the slug. Emitted as kindspec. Original frontmatter is merged andsource: 'imported'is stamped. - Memory (
*.md):MEMORY.md(the compiled view) is skipped. A file is adecisionwhen frontmatterkind === 'decision', or the slug starts withadr-/decision-, or the body matchesADR/Architecture Decision; otherwise it is atrap. - Plans (directories under the plans candidates): the primary plan file is
plan.md, elseimplementation_plan.md, else the first.mdthat is not hidden and does not containstateorlog. Emitted as kindplanwith the directory name slugified. - Plan state: a file named
.state.md,state.md,run.json, orstate.jsonin the plan folder is emitted as kindstatewith slug<planSlug>-state, titleState: <slug>, andrelatedSlugset to the plan slug. - Changelog: content is split on
^##headings; each non-empty section becomes alogrecord with sluglog-<first-30-chars-slugified>-<index>and body prefixed with##. - Status normalization (
normalizeRecordStatus):accepted/proposedbecomeactiveand additionally storedecisionStatus;active/paused/shipped/superseded/archivedpass through; anything else defaults toactive. - Skipped files: within plan folders,
.jsonl,.tmp, names containingtelemetry,audit-*, and.runtimeare counted as skipped (not imported).MEMORY.mdis likewise skipped. Files that throw during parse are counted inskippedFilesCountwith their path inskippedPaths. - Idempotency: before upserting,
findIdenticalVaultRecordlooks up the existing record by stable id (getRecordby kind + slug) and compares a SHA-256 hash oftitle,status, extra (decisionStatus), a stable frontmatter subset, and the trimmed body. Identical content is reported asskipped-identical(skippedIdenticalCountincrements) and no file is written. Changed content (body or source-stable frontmatter such asseverity,pathPatterns,linkedPaths) re-imports and overwrites. Lookup errors fail open to the normal upsert path. - After import, compiled views (
rebuildCompiledViews) and the SQLite FTS index (rebuildIndex) are rebuilt for the project. - Every imported record is stamped
source: 'imported'so provenance is visible in frontmatter.idandslugare the slugified stable key; re-running never mangles existing ids. - Re-import semantics verified by tests: a second run on an unchanged tree reports
totalImported: 0,skippedIdenticalCountequal to the first run'stotalImported, all rowsskipped-identical, and no new vault files; FTS returns exactly one hit per record. Editing source content (body or a stable frontmatter key) re-imports only that record. --fromaccepts either a product repository root or a direct.agentsdirectory; when omitted,productRoot, the first positional, orcwdis used in that order.- Human CLI output prints per-kind counts plus
Total: <n> (skipped files: <n>, identical: <n>).--jsonprints the fullImportResult. - Failures during a single file parse are non-fatal: the file is counted as skipped and the run continues. A fatal error aborts the run and records a failed telemetry event.
Technical Architecture [edit]
importWorkflowTree(options) resolves the vault root (options.vaultRoot else getVaultRoot()), the source root (options.from else productRoot else cwd else process.cwd()), and the project identity (resolveProjectIdentity), calling ensureProjectVault before any writes. It wraps importWorkflowTreeDirect and records importer/memo_import telemetry with duration, success, and counts (totalImported, skippedFilesCount, skippedIdenticalCount) on both success and failure.
Result shape (ImportResult): projectId, vaultRoot, importedSpecsCount, importedTrapsCount, importedDecisionsCount, importedPlansCount, importedLogsCount, importedStateCount, skippedFilesCount, skippedIdenticalCount, totalImported, records (imported ImportItem[] with status imported), skippedRecords (status skipped-identical), and skippedPaths.
Hash inputs are constrained by IMPORT_HASH_FM_KEYS = ['severity', 'decisionStatus', 'pathPatterns', 'linkedPaths']; vault-managed keys (id, tags, occurrences, hits, timestamps) are deliberately excluded so a vault round-trip does not look like a content change.
The importer writes through upsertRecord({ cwd, projectId, vaultRoot, kind, slug, frontmatter, body, source: 'imported' }). It does not persist an originRelPath; the original repository-relative path is not recorded on the record.
Provenance: 0005-import-and-doctor.spec.md, 0052-us-54.spec.md.