mirror-mind 0.0.1 → 0.32.1
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/.claude/hooks/log-session-end.sh +85 -0
- package/.claude/hooks/log-user-prompt.sh +85 -0
- package/.claude/hooks/mirror-inject.sh +85 -0
- package/.claude/hooks/session-start.sh +85 -0
- package/.gemini/hooks/log-assistant.sh +85 -0
- package/.gemini/hooks/log-user.sh +85 -0
- package/.gemini/hooks/session-end.sh +85 -0
- package/.gemini/hooks/session-start.sh +85 -0
- package/.pi/extensions/mirror-logger.ts +561 -0
- package/.pi/skills/mm-backup/SKILL.md +21 -0
- package/.pi/skills/mm-build/SKILL.md +1206 -0
- package/.pi/skills/mm-consolidate/SKILL.md +112 -0
- package/.pi/skills/mm-consult/SKILL.md +70 -0
- package/.pi/skills/mm-conversations/SKILL.md +16 -0
- package/.pi/skills/mm-discard/SKILL.md +39 -0
- package/.pi/skills/mm-explore/SKILL.md +255 -0
- package/.pi/skills/mm-help/SKILL.md +74 -0
- package/.pi/skills/mm-identity/SKILL.md +96 -0
- package/.pi/skills/mm-journal/SKILL.md +16 -0
- package/.pi/skills/mm-journey/SKILL.md +23 -0
- package/.pi/skills/mm-journeys/SKILL.md +15 -0
- package/.pi/skills/mm-memories/SKILL.md +17 -0
- package/.pi/skills/mm-mirror/SKILL.md +98 -0
- package/.pi/skills/mm-mute/SKILL.md +18 -0
- package/.pi/skills/mm-new/SKILL.md +18 -0
- package/.pi/skills/mm-recall/SKILL.md +17 -0
- package/.pi/skills/mm-release-notes/SKILL.md +52 -0
- package/.pi/skills/mm-seed/SKILL.md +19 -0
- package/.pi/skills/mm-shadow/SKILL.md +117 -0
- package/.pi/skills/mm-soul/SKILL.md +502 -0
- package/.pi/skills/mm-tasks/SKILL.md +24 -0
- package/.pi/skills/mm-update/SKILL.md +32 -0
- package/.pi/skills/mm-week/SKILL.md +28 -0
- package/.pi/skills/mm-welcome/SKILL.md +24 -0
- package/AGENTS.md +188 -0
- package/README.md +126 -9
- package/bin/loader.js +67 -0
- package/bin/mirror-hook.js +7 -0
- package/bin/mirror.js +7 -0
- package/docs/releases/index.md +99 -0
- package/docs/releases/pending-cutoffs.md +510 -0
- package/docs/releases/v0.10.0.md +123 -0
- package/docs/releases/v0.10.1.md +66 -0
- package/docs/releases/v0.10.10.md +51 -0
- package/docs/releases/v0.10.11.md +39 -0
- package/docs/releases/v0.10.12.md +57 -0
- package/docs/releases/v0.10.13.md +35 -0
- package/docs/releases/v0.10.14.md +42 -0
- package/docs/releases/v0.10.15.md +53 -0
- package/docs/releases/v0.10.16.md +35 -0
- package/docs/releases/v0.10.17.md +43 -0
- package/docs/releases/v0.10.2.md +64 -0
- package/docs/releases/v0.10.3.md +78 -0
- package/docs/releases/v0.10.4.md +42 -0
- package/docs/releases/v0.10.5.md +57 -0
- package/docs/releases/v0.10.6.md +47 -0
- package/docs/releases/v0.10.7.md +53 -0
- package/docs/releases/v0.10.8.md +49 -0
- package/docs/releases/v0.10.9.md +37 -0
- package/docs/releases/v0.11.0.md +86 -0
- package/docs/releases/v0.12.0.md +67 -0
- package/docs/releases/v0.13.0.md +64 -0
- package/docs/releases/v0.14.0.md +68 -0
- package/docs/releases/v0.15.0.md +48 -0
- package/docs/releases/v0.16.0.md +50 -0
- package/docs/releases/v0.16.1.md +55 -0
- package/docs/releases/v0.16.2.md +36 -0
- package/docs/releases/v0.17.0.md +50 -0
- package/docs/releases/v0.17.1.md +58 -0
- package/docs/releases/v0.17.2.md +42 -0
- package/docs/releases/v0.17.3.md +58 -0
- package/docs/releases/v0.18.0.md +84 -0
- package/docs/releases/v0.18.1.md +51 -0
- package/docs/releases/v0.18.2.md +69 -0
- package/docs/releases/v0.19.0.md +85 -0
- package/docs/releases/v0.19.1.md +46 -0
- package/docs/releases/v0.20.0.md +85 -0
- package/docs/releases/v0.21.0.md +61 -0
- package/docs/releases/v0.21.1.md +47 -0
- package/docs/releases/v0.22.0.md +77 -0
- package/docs/releases/v0.22.1.md +42 -0
- package/docs/releases/v0.23.0.md +105 -0
- package/docs/releases/v0.24.0.md +125 -0
- package/docs/releases/v0.25.0.md +107 -0
- package/docs/releases/v0.26.0.md +139 -0
- package/docs/releases/v0.26.1.md +65 -0
- package/docs/releases/v0.26.2.md +43 -0
- package/docs/releases/v0.27.0.md +63 -0
- package/docs/releases/v0.28.0.md +59 -0
- package/docs/releases/v0.29.0.md +61 -0
- package/docs/releases/v0.29.1.md +59 -0
- package/docs/releases/v0.29.2.md +59 -0
- package/docs/releases/v0.29.3.md +93 -0
- package/docs/releases/v0.30.0.md +75 -0
- package/docs/releases/v0.30.1.md +93 -0
- package/docs/releases/v0.30.2.md +66 -0
- package/docs/releases/v0.31.0.md +122 -0
- package/docs/releases/v0.31.1.md +142 -0
- package/docs/releases/v0.31.10.md +118 -0
- package/docs/releases/v0.31.11.md +88 -0
- package/docs/releases/v0.31.12.md +105 -0
- package/docs/releases/v0.31.13.md +99 -0
- package/docs/releases/v0.31.14.md +97 -0
- package/docs/releases/v0.31.2.md +115 -0
- package/docs/releases/v0.31.3.md +142 -0
- package/docs/releases/v0.31.4.md +117 -0
- package/docs/releases/v0.31.5.md +155 -0
- package/docs/releases/v0.31.6.md +132 -0
- package/docs/releases/v0.31.7.md +132 -0
- package/docs/releases/v0.31.8.md +120 -0
- package/docs/releases/v0.31.9.md +120 -0
- package/docs/releases/v0.32.0.md +261 -0
- package/docs/releases/v0.32.1.md +59 -0
- package/docs/releases/v0.8.0.md +118 -0
- package/docs/releases/v0.9.0.md +98 -0
- package/docs/releases/v0.9.1.md +83 -0
- package/package.json +102 -6
- package/plugins/mirror-mind/.claude-plugin/plugin.json +13 -0
- package/plugins/mirror-mind/hooks/hooks.json +50 -0
- package/plugins/mirror-mind/hooks/log-session-end.sh +98 -0
- package/plugins/mirror-mind/hooks/log-user-prompt.sh +98 -0
- package/plugins/mirror-mind/hooks/mirror-inject.sh +98 -0
- package/plugins/mirror-mind/hooks/session-start.sh +98 -0
- package/plugins/mirror-mind/mcp/launch.sh +52 -0
- package/plugins/mirror-mind/skills/mm-backup/SKILL.md +27 -0
- package/plugins/mirror-mind/skills/mm-build/SKILL.md +1206 -0
- package/plugins/mirror-mind/skills/mm-consolidate/SKILL.md +112 -0
- package/plugins/mirror-mind/skills/mm-consult/SKILL.md +83 -0
- package/plugins/mirror-mind/skills/mm-conversations/SKILL.md +17 -0
- package/plugins/mirror-mind/skills/mm-discard/SKILL.md +40 -0
- package/plugins/mirror-mind/skills/mm-explore/SKILL.md +241 -0
- package/plugins/mirror-mind/skills/mm-help/SKILL.md +104 -0
- package/plugins/mirror-mind/skills/mm-identity/SKILL.md +96 -0
- package/plugins/mirror-mind/skills/mm-journal/SKILL.md +21 -0
- package/plugins/mirror-mind/skills/mm-journey/SKILL.md +37 -0
- package/plugins/mirror-mind/skills/mm-journeys/SKILL.md +15 -0
- package/plugins/mirror-mind/skills/mm-memories/SKILL.md +20 -0
- package/plugins/mirror-mind/skills/mm-mirror/SKILL.md +65 -0
- package/plugins/mirror-mind/skills/mm-mute/SKILL.md +18 -0
- package/plugins/mirror-mind/skills/mm-new/SKILL.md +18 -0
- package/plugins/mirror-mind/skills/mm-recall/SKILL.md +17 -0
- package/plugins/mirror-mind/skills/mm-release-notes/SKILL.md +31 -0
- package/plugins/mirror-mind/skills/mm-seed/SKILL.md +15 -0
- package/plugins/mirror-mind/skills/mm-shadow/SKILL.md +117 -0
- package/plugins/mirror-mind/skills/mm-soul/SKILL.md +488 -0
- package/plugins/mirror-mind/skills/mm-tasks/SKILL.md +36 -0
- package/plugins/mirror-mind/skills/mm-update/SKILL.md +30 -0
- package/plugins/mirror-mind/skills/mm-week/SKILL.md +46 -0
- package/plugins/mirror-mind/skills/mm-welcome/SKILL.md +22 -0
- package/scripts/codex-hooks/session-end.sh +85 -0
- package/scripts/codex-hooks/session-start.sh +85 -0
- package/scripts/codex-mirror.sh +48 -0
- package/templates/identity/README.md +119 -0
- package/templates/identity/ego/behavior.yaml +52 -0
- package/templates/identity/ego/constraints.yaml +15 -0
- package/templates/identity/ego/identity.yaml +43 -0
- package/templates/identity/journeys/personal-growth.yaml +39 -0
- package/templates/identity/organization/identity.yaml +24 -0
- package/templates/identity/organization/principles.yaml +17 -0
- package/templates/identity/personas/coach.yaml +94 -0
- package/templates/identity/personas/designer.yaml +93 -0
- package/templates/identity/personas/doctor.yaml +96 -0
- package/templates/identity/personas/engineer.yaml +77 -0
- package/templates/identity/personas/financial.yaml +97 -0
- package/templates/identity/personas/prompt-engineer.yaml +122 -0
- package/templates/identity/personas/researcher.yaml +88 -0
- package/templates/identity/personas/strategist.yaml +91 -0
- package/templates/identity/personas/teacher.yaml +95 -0
- package/templates/identity/personas/therapist.yaml +91 -0
- package/templates/identity/personas/thinker.yaml +74 -0
- package/templates/identity/personas/writer.yaml +76 -0
- package/templates/identity/self/config.yaml +16 -0
- package/templates/identity/self/soul.yaml +105 -0
- package/templates/identity/user/identity.yaml +30 -0
- package/ts/src/backup/staging.ts +62 -0
- package/ts/src/backup/zipBackup.ts +267 -0
- package/ts/src/backup/zipReader.ts +109 -0
- package/ts/src/backup/zipWriter.ts +148 -0
- package/ts/src/builder/activeCheckpoint.ts +134 -0
- package/ts/src/builder/aggregateStatus.ts +34 -0
- package/ts/src/builder/approve.ts +149 -0
- package/ts/src/builder/argv.ts +292 -0
- package/ts/src/builder/ariadMethod.ts +536 -0
- package/ts/src/builder/artifacts/artifactSurfaces.ts +106 -0
- package/ts/src/builder/artifacts/artifactWriter.ts +113 -0
- package/ts/src/builder/artifacts/closureArtifacts.ts +199 -0
- package/ts/src/builder/artifacts/deliveryStoryArtifacts.ts +316 -0
- package/ts/src/builder/artifacts/planArtifacts.ts +242 -0
- package/ts/src/builder/artifacts/scaffoldSections.ts +273 -0
- package/ts/src/builder/artifacts/scaffoldState.ts +227 -0
- package/ts/src/builder/artifacts/storyFiles.ts +61 -0
- package/ts/src/builder/artifacts/storyIndex.ts +166 -0
- package/ts/src/builder/cadence.ts +59 -0
- package/ts/src/builder/card.ts +192 -0
- package/ts/src/builder/checkpointRefused.ts +66 -0
- package/ts/src/builder/closure.ts +964 -0
- package/ts/src/builder/commands.ts +2053 -0
- package/ts/src/builder/cursorPosition.ts +176 -0
- package/ts/src/builder/cursorTransitions.ts +77 -0
- package/ts/src/builder/deliveryCursor.ts +577 -0
- package/ts/src/builder/deliveryStoryClosure.ts +521 -0
- package/ts/src/builder/deliveryStoryPlan.ts +536 -0
- package/ts/src/builder/deliveryStoryReady.ts +160 -0
- package/ts/src/builder/deliveryStoryRoadmapClosure.ts +151 -0
- package/ts/src/builder/expand.ts +336 -0
- package/ts/src/builder/flowUnit.ts +339 -0
- package/ts/src/builder/homeSurface.ts +122 -0
- package/ts/src/builder/implementationGuard.ts +125 -0
- package/ts/src/builder/index.ts +11 -0
- package/ts/src/builder/lifecycleRefusal.ts +92 -0
- package/ts/src/builder/lifecycleRibbon.ts +159 -0
- package/ts/src/builder/load.ts +313 -0
- package/ts/src/builder/methodAdoption.ts +113 -0
- package/ts/src/builder/methodDefinition.ts +429 -0
- package/ts/src/builder/methodInspection.ts +371 -0
- package/ts/src/builder/plan.ts +408 -0
- package/ts/src/builder/planPreauthorization.ts +265 -0
- package/ts/src/builder/prepare.ts +225 -0
- package/ts/src/builder/projectPaths.ts +64 -0
- package/ts/src/builder/pull.ts +356 -0
- package/ts/src/builder/pullCandidates.ts +463 -0
- package/ts/src/builder/pullCandidatesRender.ts +238 -0
- package/ts/src/builder/refinementField.ts +93 -0
- package/ts/src/builder/releaseIntent.ts +173 -0
- package/ts/src/builder/resumeState.ts +84 -0
- package/ts/src/builder/resumeSurface.ts +149 -0
- package/ts/src/builder/roadmapGrammar.ts +300 -0
- package/ts/src/builder/roadmapScan.ts +97 -0
- package/ts/src/builder/roadmapScope.ts +210 -0
- package/ts/src/builder/scopePhrases.ts +146 -0
- package/ts/src/builder/storyPaths.ts +196 -0
- package/ts/src/builder/storyPlanPreauthorization.ts +287 -0
- package/ts/src/builder/surfaceProtocol.ts +42 -0
- package/ts/src/builder/templateGeneration.ts +148 -0
- package/ts/src/builder/transition.ts +178 -0
- package/ts/src/consult/args.ts +99 -0
- package/ts/src/consult/core.ts +122 -0
- package/ts/src/consult/modelCatalog.ts +55 -0
- package/ts/src/consult/render.ts +47 -0
- package/ts/src/conversation/append.ts +414 -0
- package/ts/src/conversation/backfill.ts +387 -0
- package/ts/src/conversation/closeTail.ts +467 -0
- package/ts/src/conversation/extraction.ts +333 -0
- package/ts/src/conversation/extractionDriver.ts +130 -0
- package/ts/src/conversation/extractionRun.ts +70 -0
- package/ts/src/conversation/journeyInference.ts +165 -0
- package/ts/src/conversation/journeyRepair.ts +114 -0
- package/ts/src/conversation/lifecycleFaces.ts +197 -0
- package/ts/src/conversation/lifecycleWrites.ts +313 -0
- package/ts/src/conversation/listing.ts +77 -0
- package/ts/src/conversation/logger.ts +576 -0
- package/ts/src/conversation/loggerCli.ts +282 -0
- package/ts/src/conversation/loggerRuntime.ts +259 -0
- package/ts/src/conversation/metadataLifecycle.ts +443 -0
- package/ts/src/conversation/recall.ts +66 -0
- package/ts/src/conversation/sessionComposites.ts +258 -0
- package/ts/src/conversation/sessionImport.ts +90 -0
- package/ts/src/conversation/transcriptBackfill.ts +141 -0
- package/ts/src/cultivation/applyActions.ts +241 -0
- package/ts/src/cultivation/cluster.ts +86 -0
- package/ts/src/cultivation/consolidationStore.ts +281 -0
- package/ts/src/cultivation/promptContext.ts +62 -0
- package/ts/src/cultivation/propose.ts +305 -0
- package/ts/src/cultivation/scan.ts +155 -0
- package/ts/src/db/backupGate.ts +47 -0
- package/ts/src/db/bootstrap.ts +108 -0
- package/ts/src/db/bootstrapLock.ts +223 -0
- package/ts/src/db/copyGuard.ts +60 -0
- package/ts/src/db/database.ts +397 -0
- package/ts/src/db/decode.ts +66 -0
- package/ts/src/db/ftsIntegrity.ts +41 -0
- package/ts/src/db/migrateOnOpen.ts +214 -0
- package/ts/src/db/migrations.ts +576 -0
- package/ts/src/db/readyOnOpen.ts +35 -0
- package/ts/src/db/rowDecode.ts +42 -0
- package/ts/src/db/schema.ts +432 -0
- package/ts/src/db/schemaInventory.ts +225 -0
- package/ts/src/db/schemaInventorySnapshot.ts +2107 -0
- package/ts/src/db/schemaState.ts +91 -0
- package/ts/src/db/schemaTsDivergence.ts +114 -0
- package/ts/src/descriptor/descriptorRead.ts +49 -0
- package/ts/src/docs/docsLint.ts +265 -0
- package/ts/src/explorer/handoff.ts +522 -0
- package/ts/src/explorer/render.ts +335 -0
- package/ts/src/explorer/story.ts +769 -0
- package/ts/src/explorer/transition.ts +72 -0
- package/ts/src/extensions/bindings.ts +170 -0
- package/ts/src/extensions/catalog.ts +249 -0
- package/ts/src/extensions/catalogCommands.ts +513 -0
- package/ts/src/extensions/catalogWrites.ts +652 -0
- package/ts/src/extensions/contextRuntime.ts +325 -0
- package/ts/src/extensions/dispatch.ts +374 -0
- package/ts/src/extensions/errors.ts +24 -0
- package/ts/src/extensions/manifest.ts +356 -0
- package/ts/src/extensions/migrations.ts +350 -0
- package/ts/src/extraction/conversation.ts +304 -0
- package/ts/src/extraction/conversationMetadata.ts +206 -0
- package/ts/src/extraction/fencing.ts +106 -0
- package/ts/src/extraction/json.ts +17 -0
- package/ts/src/extraction/prompts.ts +470 -0
- package/ts/src/frontDoor/args.ts +30 -0
- package/ts/src/frontDoor/argvShape.ts +106 -0
- package/ts/src/frontDoor/buildLoadRuntime.ts +139 -0
- package/ts/src/frontDoor/buildRoute.ts +398 -0
- package/ts/src/frontDoor/cli.ts +1865 -0
- package/ts/src/frontDoor/cloneRoleGuard.ts +98 -0
- package/ts/src/frontDoor/consultRoute.ts +50 -0
- package/ts/src/frontDoor/contentTailRoute.ts +239 -0
- package/ts/src/frontDoor/conversationLoggerRoute.ts +73 -0
- package/ts/src/frontDoor/cultivationRoute.ts +210 -0
- package/ts/src/frontDoor/dbPath.ts +131 -0
- package/ts/src/frontDoor/dbSafetyToolsRoute.ts +240 -0
- package/ts/src/frontDoor/exploreRoute.ts +572 -0
- package/ts/src/frontDoor/extensionCatalogRoute.ts +252 -0
- package/ts/src/frontDoor/frontDoorLog.ts +57 -0
- package/ts/src/frontDoor/identityWrite.ts +36 -0
- package/ts/src/frontDoor/journeyWriteRoute.ts +26 -0
- package/ts/src/frontDoor/lifecycleWriteRoute.ts +110 -0
- package/ts/src/frontDoor/liveBackup.ts +100 -0
- package/ts/src/frontDoor/mirrorModeRoute.ts +163 -0
- package/ts/src/frontDoor/namedRefusal.ts +35 -0
- package/ts/src/frontDoor/nodeSupport.ts +24 -0
- package/ts/src/frontDoor/render/consolidate.ts +199 -0
- package/ts/src/frontDoor/render/conversations.ts +33 -0
- package/ts/src/frontDoor/render/cultivationShared.ts +21 -0
- package/ts/src/frontDoor/render/descriptor.ts +20 -0
- package/ts/src/frontDoor/render/detectPersona.ts +50 -0
- package/ts/src/frontDoor/render/icons.ts +13 -0
- package/ts/src/frontDoor/render/identity.ts +59 -0
- package/ts/src/frontDoor/render/identityRows.ts +17 -0
- package/ts/src/frontDoor/render/inspectPersona.ts +53 -0
- package/ts/src/frontDoor/render/journeyStatus.ts +78 -0
- package/ts/src/frontDoor/render/journeys.ts +79 -0
- package/ts/src/frontDoor/render/list.ts +41 -0
- package/ts/src/frontDoor/render/memories.ts +67 -0
- package/ts/src/frontDoor/render/recall.ts +49 -0
- package/ts/src/frontDoor/render/shadow.ts +155 -0
- package/ts/src/frontDoor/render/tasks.ts +99 -0
- package/ts/src/frontDoor/render/tasksImportSync.ts +63 -0
- package/ts/src/frontDoor/render/week.ts +113 -0
- package/ts/src/frontDoor/routing.ts +999 -0
- package/ts/src/frontDoor/runtimeRoute.ts +623 -0
- package/ts/src/frontDoor/searchRoute.ts +166 -0
- package/ts/src/frontDoor/seedPaths.ts +98 -0
- package/ts/src/frontDoor/soulRoute.ts +433 -0
- package/ts/src/frontDoor/tasksImportSyncRoute.ts +110 -0
- package/ts/src/frontDoor/tasksWriteRoute.ts +101 -0
- package/ts/src/frontDoor/usage.ts +176 -0
- package/ts/src/guards/claudePlugin.ts +252 -0
- package/ts/src/guards/packContents.ts +306 -0
- package/ts/src/guards/piSourcedSkills.ts +70 -0
- package/ts/src/guards/retiredRouteShadows.ts +57 -0
- package/ts/src/guards/retiredSurfaces.ts +564 -0
- package/ts/src/hooks/claude.ts +176 -0
- package/ts/src/hooks/codex.ts +45 -0
- package/ts/src/hooks/gemini.ts +126 -0
- package/ts/src/hooks/main.ts +109 -0
- package/ts/src/hooks/mirrorState.ts +71 -0
- package/ts/src/hooks/payload.ts +68 -0
- package/ts/src/hooks/runtime.ts +159 -0
- package/ts/src/identity/applyConsolidationIdentityUpdate.ts +67 -0
- package/ts/src/identity/descriptorGenerate.ts +162 -0
- package/ts/src/identity/identityEdit.ts +143 -0
- package/ts/src/identity/identityKey.ts +77 -0
- package/ts/src/identity/identityRead.ts +69 -0
- package/ts/src/identity/identityStore.ts +80 -0
- package/ts/src/identity/journeyListing.ts +53 -0
- package/ts/src/identity/personaInspect.ts +44 -0
- package/ts/src/identity/personaListing.ts +37 -0
- package/ts/src/identity/setIdentity.ts +64 -0
- package/ts/src/index.ts +231 -0
- package/ts/src/init/init.ts +111 -0
- package/ts/src/journey/journeyOptions.ts +131 -0
- package/ts/src/journey/journeyRemoval.ts +158 -0
- package/ts/src/journey/journeyStatus.ts +149 -0
- package/ts/src/journey/journeySyncFile.ts +87 -0
- package/ts/src/journey/journeyWrite.ts +203 -0
- package/ts/src/journey/parentJourney.ts +41 -0
- package/ts/src/journey/validateParentJourney.ts +55 -0
- package/ts/src/mcp/boundary.ts +151 -0
- package/ts/src/mcp/guards.ts +244 -0
- package/ts/src/mcp/main.ts +121 -0
- package/ts/src/mcp/payload.ts +141 -0
- package/ts/src/mcp/protocol.ts +141 -0
- package/ts/src/mcp/readModels.ts +241 -0
- package/ts/src/mcp/registry.ts +201 -0
- package/ts/src/mcp/serve.ts +120 -0
- package/ts/src/mcp/tools/deterministic.ts +137 -0
- package/ts/src/mcp/tools/providerCrossing.ts +187 -0
- package/ts/src/mcp/wire.ts +8 -0
- package/ts/src/memory/journal.ts +180 -0
- package/ts/src/memory/listing.ts +107 -0
- package/ts/src/memory/memoryWrite.ts +88 -0
- package/ts/src/memory/reinforcement.ts +35 -0
- package/ts/src/mirror/context.ts +161 -0
- package/ts/src/mirror/defaultResolution.ts +199 -0
- package/ts/src/mirror/orchestration.ts +364 -0
- package/ts/src/mirror/reception.ts +194 -0
- package/ts/src/mirror/render.ts +75 -0
- package/ts/src/mirror/runtimeSession.ts +205 -0
- package/ts/src/mode/operatingMode.ts +148 -0
- package/ts/src/observability/callOutcome.ts +115 -0
- package/ts/src/observability/inspectLedger.ts +329 -0
- package/ts/src/observability/ledgerHooks.ts +126 -0
- package/ts/src/observability/llmCalls.ts +154 -0
- package/ts/src/persona/detectPersona.ts +81 -0
- package/ts/src/planning/promptAssembly.ts +111 -0
- package/ts/src/planning/weekPending.ts +81 -0
- package/ts/src/planning/weekPlan.ts +183 -0
- package/ts/src/planning/weekSave.ts +117 -0
- package/ts/src/providers/config.ts +189 -0
- package/ts/src/providers/cost.ts +64 -0
- package/ts/src/providers/credits.ts +204 -0
- package/ts/src/providers/embedding.ts +321 -0
- package/ts/src/providers/familyProviders.ts +147 -0
- package/ts/src/providers/llm.ts +334 -0
- package/ts/src/providers/openrouter.ts +244 -0
- package/ts/src/providers/redaction.ts +110 -0
- package/ts/src/providers/replay.ts +15 -0
- package/ts/src/providers/transport.ts +410 -0
- package/ts/src/repair/encodingRepair.ts +253 -0
- package/ts/src/runtime/backup.ts +151 -0
- package/ts/src/runtime/channel.ts +146 -0
- package/ts/src/runtime/config.ts +168 -0
- package/ts/src/runtime/diagnose.ts +686 -0
- package/ts/src/runtime/git.ts +510 -0
- package/ts/src/runtime/installKind.ts +220 -0
- package/ts/src/runtime/migrate.ts +154 -0
- package/ts/src/runtime/packageIdentity.ts +102 -0
- package/ts/src/runtime/releaseNotes.ts +244 -0
- package/ts/src/runtime/status.ts +557 -0
- package/ts/src/runtime/strategies/clone.ts +200 -0
- package/ts/src/runtime/strategies/package.ts +328 -0
- package/ts/src/runtime/treeRoot.ts +30 -0
- package/ts/src/runtime/update.ts +445 -0
- package/ts/src/runtime/updateGate.ts +40 -0
- package/ts/src/runtime/updatePipeline.ts +159 -0
- package/ts/src/runtime/version.ts +36 -0
- package/ts/src/runtime/wiring.ts +130 -0
- package/ts/src/search/memorySearch.ts +338 -0
- package/ts/src/search/ranker.ts +154 -0
- package/ts/src/seed/seed.ts +256 -0
- package/ts/src/seed/seedYaml.ts +133 -0
- package/ts/src/soul/apply.ts +218 -0
- package/ts/src/soul/harvest.ts +267 -0
- package/ts/src/soul/prompts/soul_beauty_voice.md +103 -0
- package/ts/src/soul/prompts/soul_self_voice.md +219 -0
- package/ts/src/soul/prompts/soul_wisdom_voice.md +121 -0
- package/ts/src/soul/prompts.ts +85 -0
- package/ts/src/soul/render.ts +358 -0
- package/ts/src/soul/state.ts +212 -0
- package/ts/src/soul/transition.ts +37 -0
- package/ts/src/tasks/journeyPathParse.ts +113 -0
- package/ts/src/tasks/taskImportSync.ts +142 -0
- package/ts/src/tasks/taskStore.ts +312 -0
- package/ts/src/tasks/weekView.ts +120 -0
- package/ts/src/util/clipCodePoints.ts +26 -0
- package/ts/src/util/paths.ts +63 -0
- package/ts/src/util/program.ts +9 -0
- package/ts/src/util/pyGenerators.ts +144 -0
- package/ts/src/util/pythonText.ts +317 -0
- package/ts/src/util/semver.ts +45 -0
- package/ts/src/util/shellWord.ts +25 -0
- package/ts/src/util/slug.ts +64 -0
- package/ts/src/welcome/card.ts +339 -0
- package/ts/src/welcome/statusLine.ts +101 -0
- package/ts/src/welcome/updateCache.ts +129 -0
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
// Shared machinery for every Mirror hook entry.
|
|
2
|
+
//
|
|
3
|
+
// Two jobs, both learned from what the Python hooks did and what they hid.
|
|
4
|
+
//
|
|
5
|
+
// **Reuse the front door rather than reimplement it.** `cli.ts` exports
|
|
6
|
+
// `main(argv)`, so a hook runs the exact route a skill or the Pi extension
|
|
7
|
+
// runs -- same routing, same front-door log, same guards -- in ONE process.
|
|
8
|
+
// The Claude inject hook used to start the interpreter up to five times per
|
|
9
|
+
// prompt; it now starts Node once.
|
|
10
|
+
//
|
|
11
|
+
// **Never fail silently.** Every Python hook ended in `2>/dev/null || true`,
|
|
12
|
+
// which was survivable because `python3` is on every macOS at /usr/bin. `node`
|
|
13
|
+
// usually is not: it lives under nvm or Homebrew, and a GUI-launched runtime
|
|
14
|
+
// does not inherit that PATH. The same silence would therefore hide a NEW
|
|
15
|
+
// failure mode -- no logging, no inject, no close tail -- discovered weeks
|
|
16
|
+
// later as "Mirror stopped remembering". So a hook that cannot do its job
|
|
17
|
+
// writes one line to `<mirror-home>/hooks.log`, and `runtime diagnose` reports
|
|
18
|
+
// whether Node is resolvable from hook context.
|
|
19
|
+
|
|
20
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, realpathSync } from "node:fs";
|
|
21
|
+
import { dirname, join } from "node:path";
|
|
22
|
+
|
|
23
|
+
import { resolveMirrorHome } from "#frontDoor/dbPath.ts";
|
|
24
|
+
import { runningTreeRoot } from "#runtime/treeRoot.ts";
|
|
25
|
+
|
|
26
|
+
/** Where a hook records that it could not do its job. */
|
|
27
|
+
export function hookLogPath(env: NodeJS.ProcessEnv = process.env): string | null {
|
|
28
|
+
try {
|
|
29
|
+
const home = resolveMirrorHome(env as Parameters<typeof resolveMirrorHome>[0]);
|
|
30
|
+
return join(home, "hooks.log");
|
|
31
|
+
} catch {
|
|
32
|
+
// An unconfigured home has nowhere to complain to. Better than throwing
|
|
33
|
+
// from inside a hook, which would surface as a broken turn.
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Append one line about a hook that could not complete.
|
|
40
|
+
*
|
|
41
|
+
* Redaction is the contract, not a nicety: this file records that something
|
|
42
|
+
* failed and which hook it was, NEVER the prompt or the response. The
|
|
43
|
+
* front-door log has the same rule (`hookPayloadRedaction.test.ts`), and hook
|
|
44
|
+
* entries bypass the CLI where that rule is enforced, so it is restated here
|
|
45
|
+
* and tested directly.
|
|
46
|
+
*/
|
|
47
|
+
export function noteHookFailure(hook: string, reason: string, env = process.env): void {
|
|
48
|
+
const path = hookLogPath(env);
|
|
49
|
+
if (!path) return;
|
|
50
|
+
try {
|
|
51
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
52
|
+
const line = `${new Date().toISOString()} ${hook}: ${reason.split("\n")[0]}\n`;
|
|
53
|
+
appendFileSync(path, line, { encoding: "utf8", mode: 0o600 });
|
|
54
|
+
} catch {
|
|
55
|
+
// A hook cannot report that it could not report. Stop here.
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The Operating Instructions a SessionStart hook should hand its runtime, or
|
|
61
|
+
* null when the project already carries this same file.
|
|
62
|
+
*
|
|
63
|
+
* CV22.DS10.US3, decision D13. An installed package reaches Claude Code and
|
|
64
|
+
* Gemini CLI through hooks, skills, and an MCP server; none of those reads a
|
|
65
|
+
* CLAUDE.md or GEMINI.md, so a session wired to the package would answer
|
|
66
|
+
* `/mm-mirror` and still route no mode, sign no persona, and hold no Builder
|
|
67
|
+
* boundary. Both runtimes read SessionStart's
|
|
68
|
+
* `hookSpecificOutput.additionalContext`, so the session-start hook returns
|
|
69
|
+
* `AGENTS.md` from the tree THIS code runs from -- the instructions always
|
|
70
|
+
* match the core they ship with -- unless the project the session opened in
|
|
71
|
+
* is that same tree (the checkout, whose CLAUDE.md imports the same file), in
|
|
72
|
+
* which case a second copy is noise. A file that cannot be read is recorded:
|
|
73
|
+
* a Mirror session without its instructions is not a Mirror session.
|
|
74
|
+
*/
|
|
75
|
+
export function operatingInstructionsFor(
|
|
76
|
+
hook: string,
|
|
77
|
+
projectDir: string | undefined,
|
|
78
|
+
): string | null {
|
|
79
|
+
try {
|
|
80
|
+
const ours = join(runningTreeRoot(), "AGENTS.md");
|
|
81
|
+
const theirs = projectDir ? join(projectDir, "AGENTS.md") : "";
|
|
82
|
+
if (theirs && existsSync(theirs) && realpathSync(theirs) === realpathSync(ours)) return null;
|
|
83
|
+
return readFileSync(ours, "utf8");
|
|
84
|
+
} catch (error) {
|
|
85
|
+
noteHookFailure(
|
|
86
|
+
hook,
|
|
87
|
+
`operating instructions not delivered: ${error instanceof Error ? error.message : String(error)}`,
|
|
88
|
+
);
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* The SessionStart answer both runtimes read: context as JSON, or `nothing`
|
|
95
|
+
* -- what each runtime's hook printed before D13 (Gemini `{}`, Claude none).
|
|
96
|
+
*/
|
|
97
|
+
export function sessionStartOutput(instructions: string | null, nothing = ""): string {
|
|
98
|
+
if (instructions === null) return nothing;
|
|
99
|
+
return `${JSON.stringify({
|
|
100
|
+
hookSpecificOutput: { hookEventName: "SessionStart", additionalContext: instructions },
|
|
101
|
+
})}\n`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface FrontDoorRun {
|
|
105
|
+
readonly code: number;
|
|
106
|
+
readonly stdout: string;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Run a front-door command in this process, capturing what it printed.
|
|
111
|
+
*
|
|
112
|
+
* The capture is what lets the inject hook decide whether context was actually
|
|
113
|
+
* produced -- the shell version tested `[ -n "$CONTEXT" ]` on command
|
|
114
|
+
* substitution, and this is the same test without a second process.
|
|
115
|
+
*/
|
|
116
|
+
export async function runFrontDoor(argv: readonly string[]): Promise<FrontDoorRun> {
|
|
117
|
+
const { main } = await import("#frontDoor/cli.ts");
|
|
118
|
+
const chunks: string[] = [];
|
|
119
|
+
const original = process.stdout.write.bind(process.stdout);
|
|
120
|
+
// biome-ignore lint/suspicious/noExplicitAny: matching Node's overloaded write signature
|
|
121
|
+
(process.stdout as any).write = (chunk: any, ...rest: any[]): boolean => {
|
|
122
|
+
chunks.push(typeof chunk === "string" ? chunk : String(chunk));
|
|
123
|
+
const callback = rest.find((argument) => typeof argument === "function");
|
|
124
|
+
if (callback) callback();
|
|
125
|
+
return true;
|
|
126
|
+
};
|
|
127
|
+
try {
|
|
128
|
+
const code = await main([...argv]);
|
|
129
|
+
return { code, stdout: chunks.join("") };
|
|
130
|
+
} finally {
|
|
131
|
+
process.stdout.write = original;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* The command a failure line may name: the family and its subcommand, never
|
|
137
|
+
* the arguments. Arguments are where hooks put what the user said and what the
|
|
138
|
+
* model answered -- the Gemini logger passes the prompt and the response as
|
|
139
|
+
* argv -- and the first version of the failure line printed all of them (TS5
|
|
140
|
+
* handoff review, finding P1).
|
|
141
|
+
*/
|
|
142
|
+
function commandName(argv: readonly string[]): string {
|
|
143
|
+
return argv.slice(0, 2).join(" ");
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Run a front-door command for its effect, swallowing failure the way the
|
|
148
|
+
* shell hooks did with `|| true` -- but recording it.
|
|
149
|
+
*/
|
|
150
|
+
export async function runFrontDoorQuietly(hook: string, argv: readonly string[]): Promise<void> {
|
|
151
|
+
try {
|
|
152
|
+
const result = await runFrontDoor(argv);
|
|
153
|
+
if (result.code !== 0) {
|
|
154
|
+
noteHookFailure(hook, `\`${commandName(argv)}\` exited ${result.code}`);
|
|
155
|
+
}
|
|
156
|
+
} catch (error) {
|
|
157
|
+
noteHookFailure(hook, error instanceof Error ? error.message : String(error));
|
|
158
|
+
}
|
|
159
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// TS port of `IdentityService.apply_consolidation_identity_update` (AI-23,
|
|
2
|
+
// CV9.E2.S23) -- the identity-write allowlist gate behind `consolidate apply`'s
|
|
3
|
+
// `identity_update` action (CV22.DS7.US3).
|
|
4
|
+
//
|
|
5
|
+
// Unlike `setIdentity` (general-purpose, unconditional write used for seeding,
|
|
6
|
+
// Soul Mode integration, `identity set`, etc.), this is narrowly for the
|
|
7
|
+
// propose_consolidation() -> mm-consolidate accept flow: `target_layer` is
|
|
8
|
+
// model-chosen and untrusted, so it is checked against
|
|
9
|
+
// `VALID_IDENTITY_UPDATE_LAYERS` before anything is written. A rejected layer
|
|
10
|
+
// throws -- it never silently no-ops, never redirects, and never writes a
|
|
11
|
+
// partial row. This is the ONE path `consolidate apply`'s `identity_update`
|
|
12
|
+
// action may reach an identity write through; it must never call the ungated
|
|
13
|
+
// `upsertIdentity`/`setIdentity` directly for that action.
|
|
14
|
+
//
|
|
15
|
+
// Preserves the accept flow's existing append semantics for an allowed write:
|
|
16
|
+
// new content is appended after a blank line, never replaces existing content
|
|
17
|
+
// outright -- the same shape as Python's `f"{existing.content.rstrip()}\n\n{content}"`.
|
|
18
|
+
|
|
19
|
+
import type { WritableDatabase } from "#db/database.ts";
|
|
20
|
+
import { getIdentityContent } from "./identityRead.ts";
|
|
21
|
+
import { type IdentityRow, upsertIdentity } from "./identityStore.ts";
|
|
22
|
+
|
|
23
|
+
/** Mirrors `models.VALID_IDENTITY_UPDATE_LAYERS = frozenset({"self", "ego"})`. */
|
|
24
|
+
export const VALID_IDENTITY_UPDATE_LAYERS: ReadonlySet<string> = new Set(["self", "ego"]);
|
|
25
|
+
|
|
26
|
+
export interface ApplyConsolidationIdentityUpdateParams {
|
|
27
|
+
targetLayer: string;
|
|
28
|
+
targetKey: string;
|
|
29
|
+
content: string;
|
|
30
|
+
/** Injected id/now, the DS4 determinism idiom -- used only on INSERT. */
|
|
31
|
+
id: string;
|
|
32
|
+
nowIso: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Refuse a non-allowlisted `targetLayer` with the exact Python message and no
|
|
37
|
+
* write at all (no row, no partial row); otherwise append-or-create at parity.
|
|
38
|
+
*
|
|
39
|
+
* The thrown message is byte-identical to Python's:
|
|
40
|
+
* `Refusing identity_update to layer {target_layer!r}: not in the
|
|
41
|
+
* consolidation allowlist {sorted(VALID_IDENTITY_UPDATE_LAYERS)}.` --
|
|
42
|
+
* `sorted({"self", "ego"})` is `['ego', 'self']`, and Python's `!r` repr of a
|
|
43
|
+
* str is single-quoted.
|
|
44
|
+
*/
|
|
45
|
+
export function applyConsolidationIdentityUpdate(
|
|
46
|
+
db: WritableDatabase,
|
|
47
|
+
params: ApplyConsolidationIdentityUpdateParams,
|
|
48
|
+
): void {
|
|
49
|
+
if (!VALID_IDENTITY_UPDATE_LAYERS.has(params.targetLayer)) {
|
|
50
|
+
throw new Error(
|
|
51
|
+
`Refusing identity_update to layer '${params.targetLayer}': not in ` +
|
|
52
|
+
"the consolidation allowlist ['ego', 'self'].",
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
const existingContent = getIdentityContent(db, params.targetLayer, params.targetKey);
|
|
56
|
+
const updatedContent =
|
|
57
|
+
existingContent !== null ? `${existingContent.trimEnd()}\n\n${params.content}` : params.content;
|
|
58
|
+
const row: IdentityRow = {
|
|
59
|
+
id: params.id,
|
|
60
|
+
layer: params.targetLayer,
|
|
61
|
+
key: params.targetKey,
|
|
62
|
+
content: updatedContent,
|
|
63
|
+
version: "1.0.0",
|
|
64
|
+
metadata: null,
|
|
65
|
+
};
|
|
66
|
+
upsertIdentity(db, row, params.nowIso);
|
|
67
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `descriptor generate` — CV22.DS7.US11 plateau 5.
|
|
3
|
+
*
|
|
4
|
+
* Generates a routing descriptor per identity and upserts it. CR068 found this
|
|
5
|
+
* leaf unowned: DS7.US1 kept it on Python "as the DS7↔DS8 live seam", but DS8
|
|
6
|
+
* flips live mode and ports nothing, so its orchestration belongs here.
|
|
7
|
+
*
|
|
8
|
+
* Graded by `ts/test/goldens/descriptor.golden.json`. Two behaviours the
|
|
9
|
+
* corpus pins:
|
|
10
|
+
*
|
|
11
|
+
* - **No ledger row.** `_cmd_generate` calls `generate_descriptor` WITHOUT an
|
|
12
|
+
* `on_llm_call`, making this the only LLM role in the product that writes
|
|
13
|
+
* nothing to `llm_calls`. The corpus records zero rows so a port cannot
|
|
14
|
+
* "helpfully" start logging and diverge. Closing the gap is a DS8 input.
|
|
15
|
+
* - **The 80-character preview is CODE POINTS**, and the ellipsis appears
|
|
16
|
+
* only when the descriptor is strictly longer than 80.
|
|
17
|
+
*
|
|
18
|
+
* `generate_descriptor` itself returns `""` for empty content BEFORE calling
|
|
19
|
+
* the model, and `""` on any provider exception — both land on the same
|
|
20
|
+
* "skipped (empty response)" line, and the target still counts in the
|
|
21
|
+
* denominator.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import type { Database, WritableDatabase } from "#db/database.ts";
|
|
25
|
+
import { codePointLength, sliceCodePoints } from "#util/pythonText.ts";
|
|
26
|
+
|
|
27
|
+
export interface DescriptorTarget {
|
|
28
|
+
layer: string;
|
|
29
|
+
key: string;
|
|
30
|
+
content: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface IdentityRow {
|
|
34
|
+
key: string;
|
|
35
|
+
content: string | null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Python `store.get_identity_by_layer(layer)` ordering. */
|
|
39
|
+
export function identitiesInLayer(db: Database, layer: string): IdentityRow[] {
|
|
40
|
+
return db
|
|
41
|
+
.prepare("SELECT key, content FROM identity WHERE layer = ? ORDER BY key")
|
|
42
|
+
.all(layer) as unknown as IdentityRow[];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface ResolveTargetsInput {
|
|
46
|
+
layer?: string | null;
|
|
47
|
+
key?: string | null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export type ResolveTargetsResult =
|
|
51
|
+
| { kind: "targets"; targets: DescriptorTarget[] }
|
|
52
|
+
| { kind: "missing-identity"; layer: string; key: string };
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Python's target selection: a specific pair, a whole layer, or — with no
|
|
56
|
+
* flags — every persona THEN every journey, in that order.
|
|
57
|
+
*/
|
|
58
|
+
export function resolveDescriptorTargets(
|
|
59
|
+
db: Database,
|
|
60
|
+
input: ResolveTargetsInput,
|
|
61
|
+
): ResolveTargetsResult {
|
|
62
|
+
if (input.layer && input.key) {
|
|
63
|
+
const row = db
|
|
64
|
+
.prepare("SELECT key, content FROM identity WHERE layer = ? AND key = ?")
|
|
65
|
+
.get(input.layer, input.key) as unknown as IdentityRow | undefined;
|
|
66
|
+
if (!row) return { kind: "missing-identity", layer: input.layer, key: input.key };
|
|
67
|
+
return {
|
|
68
|
+
kind: "targets",
|
|
69
|
+
targets: [{ layer: input.layer, key: input.key, content: row.content ?? "" }],
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
if (input.layer) {
|
|
73
|
+
return {
|
|
74
|
+
kind: "targets",
|
|
75
|
+
targets: identitiesInLayer(db, input.layer).map((row) => ({
|
|
76
|
+
layer: input.layer as string,
|
|
77
|
+
key: row.key,
|
|
78
|
+
content: row.content ?? "",
|
|
79
|
+
})),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
const targets: DescriptorTarget[] = [];
|
|
83
|
+
for (const layer of ["persona", "journey"]) {
|
|
84
|
+
for (const row of identitiesInLayer(db, layer)) {
|
|
85
|
+
targets.push({ layer, key: row.key, content: row.content ?? "" });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return { kind: "targets", targets };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Python `upsert_descriptor`. */
|
|
92
|
+
export function upsertDescriptor(
|
|
93
|
+
db: WritableDatabase,
|
|
94
|
+
layer: string,
|
|
95
|
+
key: string,
|
|
96
|
+
descriptor: string,
|
|
97
|
+
generatedAt: string,
|
|
98
|
+
): void {
|
|
99
|
+
db.prepare(
|
|
100
|
+
"INSERT INTO identity_descriptors (layer, key, descriptor, generated_at) VALUES (?, ?, ?, ?) " +
|
|
101
|
+
"ON CONFLICT(layer, key) DO UPDATE SET descriptor = excluded.descriptor, generated_at = excluded.generated_at",
|
|
102
|
+
).run(layer, key, descriptor, generatedAt);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Python: `f"done — {descriptor[:80]}{'...' if len(descriptor) > 80 else ''}"`. */
|
|
106
|
+
export function descriptorPreview(descriptor: string): string {
|
|
107
|
+
const head = sliceCodePoints(descriptor, 80);
|
|
108
|
+
return codePointLength(descriptor) > 80 ? `${head}...` : head;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export interface DescriptorGenerateDeps {
|
|
112
|
+
/** Returns the descriptor text, already stripped; `""` means skip. */
|
|
113
|
+
generate: (target: DescriptorTarget) => string;
|
|
114
|
+
nowIso: string;
|
|
115
|
+
print?: (line: string) => void;
|
|
116
|
+
printError?: (line: string) => void;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export interface DescriptorGenerateResult {
|
|
120
|
+
exitCode: number;
|
|
121
|
+
generated: number;
|
|
122
|
+
total: number;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function runDescriptorGenerate(
|
|
126
|
+
db: WritableDatabase,
|
|
127
|
+
input: ResolveTargetsInput,
|
|
128
|
+
deps: DescriptorGenerateDeps,
|
|
129
|
+
): DescriptorGenerateResult {
|
|
130
|
+
const print = deps.print ?? ((line: string) => process.stdout.write(line));
|
|
131
|
+
const printError = deps.printError ?? ((line: string) => process.stderr.write(line));
|
|
132
|
+
|
|
133
|
+
const resolved = resolveDescriptorTargets(db, input);
|
|
134
|
+
if (resolved.kind === "missing-identity") {
|
|
135
|
+
// Python's `!r` renders Python string repr: single quotes.
|
|
136
|
+
printError(`No identity found for layer='${resolved.layer}' key='${resolved.key}'\n`);
|
|
137
|
+
return { exitCode: 1, generated: 0, total: 0 };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const { targets } = resolved;
|
|
141
|
+
if (targets.length === 0) {
|
|
142
|
+
print("No entities found to describe.\n");
|
|
143
|
+
return { exitCode: 0, generated: 0, total: 0 };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
let generated = 0;
|
|
147
|
+
for (const target of targets) {
|
|
148
|
+
// Python prints with `end=" "`, so the line stays open until the outcome.
|
|
149
|
+
print(` generating ${target.layer}/${target.key}... `);
|
|
150
|
+
const descriptor = deps.generate(target);
|
|
151
|
+
if (descriptor) {
|
|
152
|
+
upsertDescriptor(db, target.layer, target.key, descriptor, deps.nowIso);
|
|
153
|
+
print(`done — ${descriptorPreview(descriptor)}\n`);
|
|
154
|
+
generated += 1;
|
|
155
|
+
} else {
|
|
156
|
+
print("skipped (empty response)\n");
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
print(`\n${generated}/${targets.length} descriptors generated.\n`);
|
|
161
|
+
return { exitCode: 0, generated, total: targets.length };
|
|
162
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// `identity edit <layer> <key>` (CV22.DS7.TS4 plateau 6, Scope F).
|
|
2
|
+
//
|
|
3
|
+
// Port of `cmd_edit` in `src/memory/cli/identity_cmd.py`. The command is small
|
|
4
|
+
// and its risk is not: it hands a human's identity content to an arbitrary
|
|
5
|
+
// program and saves whatever comes back, so the three refusals matter more than
|
|
6
|
+
// the happy path.
|
|
7
|
+
//
|
|
8
|
+
// * **A failed editor saves nothing.** A non-zero exit is an abort, not an
|
|
9
|
+
// empty buffer -- someone who quits `vim` with `:cq` means "discard".
|
|
10
|
+
// * **Blank content is refused, never stored.** An identity layer emptied by
|
|
11
|
+
// accident is a silent loss the next Mirror Mode load would inherit.
|
|
12
|
+
// * **An unchanged buffer writes nothing at all**, so `updated_at` does not
|
|
13
|
+
// move and the content's history stays honest.
|
|
14
|
+
//
|
|
15
|
+
// The temp file carries the content of a person's identity, so it is created
|
|
16
|
+
// 0600 (Python gets this from `NamedTemporaryFile`) and removed on EVERY path,
|
|
17
|
+
// including the ones that refuse. Both are asserted by tests rather than
|
|
18
|
+
// assumed: they are the security-engineer's plan-stage requirement.
|
|
19
|
+
|
|
20
|
+
import { spawnSync } from "node:child_process";
|
|
21
|
+
import { closeSync, openSync, readFileSync, rmSync, writeSync } from "node:fs";
|
|
22
|
+
import { tmpdir } from "node:os";
|
|
23
|
+
import { join } from "node:path";
|
|
24
|
+
import type { WritableDatabase } from "#db/database.ts";
|
|
25
|
+
import { applyIdentitySet } from "#frontDoor/identityWrite.ts";
|
|
26
|
+
|
|
27
|
+
export interface IdentityEditResult {
|
|
28
|
+
stdout: string;
|
|
29
|
+
stderr: string;
|
|
30
|
+
exitCode: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface IdentityEditDeps {
|
|
34
|
+
/** The editor argv[0], resolved the way Python resolves it. */
|
|
35
|
+
editor?: string | undefined;
|
|
36
|
+
visual?: string | undefined;
|
|
37
|
+
nowIso: () => string;
|
|
38
|
+
/** Injected so a test can watch the buffer without driving a real editor. */
|
|
39
|
+
spawn?: typeof spawnSync;
|
|
40
|
+
/** Injected so a test can assert the temp file is gone afterwards. */
|
|
41
|
+
onTempFile?: (path: string) => void;
|
|
42
|
+
newId: () => string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Python's `os.environ.get("EDITOR") or os.environ.get("VISUAL") or "nano"`. */
|
|
46
|
+
export function resolveEditor(deps: IdentityEditDeps): string {
|
|
47
|
+
return deps.editor || deps.visual || "nano";
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Python's `NamedTemporaryFile(prefix=..., suffix=".md", delete=False)`: a
|
|
52
|
+
* fresh name in the system temp directory, created 0600 by the open itself.
|
|
53
|
+
*
|
|
54
|
+
* `wx` is what makes it safe: creating with exclusive intent means the command
|
|
55
|
+
* can never write identity content into a path that already exists, which is
|
|
56
|
+
* the whole difference between a temp file and a target of someone else's
|
|
57
|
+
* choosing.
|
|
58
|
+
*/
|
|
59
|
+
function writePrivateTempFile(prefix: string, content: string): string {
|
|
60
|
+
for (let attempt = 0; attempt < 10; attempt += 1) {
|
|
61
|
+
const suffix = Math.random().toString(36).slice(2, 10);
|
|
62
|
+
const path = join(tmpdir(), `${prefix}${suffix}.md`);
|
|
63
|
+
try {
|
|
64
|
+
const handle = openSync(path, "wx", 0o600);
|
|
65
|
+
try {
|
|
66
|
+
writeSync(handle, content);
|
|
67
|
+
} finally {
|
|
68
|
+
closeSync(handle);
|
|
69
|
+
}
|
|
70
|
+
return path;
|
|
71
|
+
} catch (error) {
|
|
72
|
+
if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
throw new Error("could not create a private temporary file for identity edit");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Port of `cmd_edit`. */
|
|
79
|
+
export function runIdentityEdit(
|
|
80
|
+
db: WritableDatabase,
|
|
81
|
+
layer: string,
|
|
82
|
+
key: string,
|
|
83
|
+
deps: IdentityEditDeps,
|
|
84
|
+
): IdentityEditResult {
|
|
85
|
+
const existing = db
|
|
86
|
+
.prepare("SELECT content FROM identity WHERE layer = ? AND key = ?")
|
|
87
|
+
.get(layer, key) as { content: string } | undefined;
|
|
88
|
+
const currentContent = existing?.content ?? "";
|
|
89
|
+
const editor = resolveEditor(deps);
|
|
90
|
+
|
|
91
|
+
const tempPath = writePrivateTempFile(`mirror-identity-${layer}-${key}-`, currentContent);
|
|
92
|
+
deps.onTempFile?.(tempPath);
|
|
93
|
+
|
|
94
|
+
let newContent: string;
|
|
95
|
+
try {
|
|
96
|
+
const spawn = deps.spawn ?? spawnSync;
|
|
97
|
+
const result = spawn(editor, [tempPath], { stdio: "inherit", shell: false });
|
|
98
|
+
if (result.error) {
|
|
99
|
+
// Python raises FileNotFoundError here and the traceback escapes. Same
|
|
100
|
+
// exit code, one line, on the same stream: the recorded divergence class.
|
|
101
|
+
return {
|
|
102
|
+
stdout: "",
|
|
103
|
+
stderr: `Editor '${editor}' could not be started.\n`,
|
|
104
|
+
exitCode: 1,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
if (result.status !== 0) {
|
|
108
|
+
return {
|
|
109
|
+
stdout: "",
|
|
110
|
+
stderr: `Editor exited with code ${result.status}. Aborted.\n`,
|
|
111
|
+
exitCode: 1,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
newContent = readFileSync(tempPath, "utf8");
|
|
115
|
+
} finally {
|
|
116
|
+
// Every path, including the refusals above: the buffer holds identity
|
|
117
|
+
// content and must not outlive the command.
|
|
118
|
+
rmSync(tempPath, { force: true });
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (!newContent.trim()) {
|
|
122
|
+
return {
|
|
123
|
+
stdout: "",
|
|
124
|
+
stderr: "Content is empty after editing. No changes saved.\n",
|
|
125
|
+
exitCode: 1,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
if (newContent === currentContent) {
|
|
129
|
+
return { stdout: "No changes detected.\n", stderr: "", exitCode: 0 };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// The same write `identity set` routes through, including its created/updated
|
|
133
|
+
// verb: two commands that disagree about whether a key existed would be two
|
|
134
|
+
// truths about one row.
|
|
135
|
+
const outcome = applyIdentitySet(db, {
|
|
136
|
+
layer,
|
|
137
|
+
key,
|
|
138
|
+
content: newContent,
|
|
139
|
+
id: deps.newId(),
|
|
140
|
+
nowIso: deps.nowIso(),
|
|
141
|
+
});
|
|
142
|
+
return { stdout: `✓ ${layer}/${key} ${outcome.action}\n`, stderr: "", exitCode: 0 };
|
|
143
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// CR104 — the identity keys agents put into commands.
|
|
2
|
+
//
|
|
3
|
+
// A journey slug and a persona id are identity keys, and they do not stay in the
|
|
4
|
+
// database: `--journey <slug>` goes into every Builder command, and the id that
|
|
5
|
+
// `detect-persona` prints goes into `mirror load --persona <id>`. Agents write
|
|
6
|
+
// those commands. A key that holds shell syntax is therefore two steps from a
|
|
7
|
+
// command on the user's machine: a crafted key, then a command that carries it.
|
|
8
|
+
// Persona ids can arrive from a stranger, in a persona bundle made to be shared
|
|
9
|
+
// and seeded.
|
|
10
|
+
//
|
|
11
|
+
// So a NEW key in these layers must be a kebab-case slug (`isKebabSlug`), checked
|
|
12
|
+
// where every creation passes: `upsertIdentity`'s INSERT branch. `identity set`
|
|
13
|
+
// and `identity edit` ask the same question earlier, before the write seam takes
|
|
14
|
+
// its snapshot and before an editor opens. A key that already exists predates
|
|
15
|
+
// the rule and stays writable; `runtime diagnose` reports it instead
|
|
16
|
+
// (`identityKeyBreaksGrammar`).
|
|
17
|
+
//
|
|
18
|
+
// Nothing here normalizes a key. A key stored differently from how it was typed
|
|
19
|
+
// would miss the next command that uses the typed one, so the answer is a refusal
|
|
20
|
+
// that names a key that would work.
|
|
21
|
+
|
|
22
|
+
import { pyRepr } from "#util/pythonText.ts";
|
|
23
|
+
import { isKebabSlug, kebabSlug } from "#util/slug.ts";
|
|
24
|
+
|
|
25
|
+
/** What each guarded layer calls its key, in the refusal. */
|
|
26
|
+
const KEY_NAMES: ReadonlyMap<string, string> = new Map([
|
|
27
|
+
["journey", "journey slug"],
|
|
28
|
+
["persona", "persona id"],
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
/** The layers whose keys agents put into commands, and so must be kebab-case slugs. */
|
|
32
|
+
export const SLUG_KEYED_LAYERS: readonly string[] = [...KEY_NAMES.keys()];
|
|
33
|
+
|
|
34
|
+
/** Whether `key` breaks the grammar its layer requires. Always false for other layers. */
|
|
35
|
+
export function identityKeyBreaksGrammar(layer: string, key: string): boolean {
|
|
36
|
+
return KEY_NAMES.has(layer) && !isKebabSlug(key);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Why a NEW row in `layer` cannot take `key`, or null when it can. The sentence
|
|
41
|
+
* leads with the consequence, because the reader is often an agent deciding its
|
|
42
|
+
* next move, then gives the rule and a key that would work. The key is printed
|
|
43
|
+
* escaped, so a newline or a terminal escape inside it cannot forge output.
|
|
44
|
+
*/
|
|
45
|
+
export function newIdentityKeyProblem(layer: string, key: string): string | null {
|
|
46
|
+
const keyName = KEY_NAMES.get(layer);
|
|
47
|
+
if (keyName === undefined || isKebabSlug(key)) return null;
|
|
48
|
+
const suggestion = kebabSlug(key);
|
|
49
|
+
const example = suggestion ? `, for example ${pyRepr(suggestion)}` : "";
|
|
50
|
+
return (
|
|
51
|
+
`no ${layer} was created: ${pyRepr(key)} is not a ${keyName}. ` +
|
|
52
|
+
`Use lowercase letters, digits, and single hyphens, up to 80 characters${example}.`
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Thrown when a new identity row's key breaks its layer's grammar. Nothing was written.
|
|
58
|
+
* A route that did not ask first lets it through, and the front door answers it as
|
|
59
|
+
* one `Error:` line (`#frontDoor/namedRefusal.ts`).
|
|
60
|
+
*/
|
|
61
|
+
export class InvalidIdentityKeyError extends Error {
|
|
62
|
+
readonly layer: string;
|
|
63
|
+
readonly key: string;
|
|
64
|
+
|
|
65
|
+
constructor(layer: string, key: string, problem: string) {
|
|
66
|
+
super(problem);
|
|
67
|
+
this.name = "InvalidIdentityKeyError";
|
|
68
|
+
this.layer = layer;
|
|
69
|
+
this.key = key;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Throw `InvalidIdentityKeyError` when a new row in `layer` cannot take `key`. */
|
|
74
|
+
export function assertNewIdentityKey(layer: string, key: string): void {
|
|
75
|
+
const problem = newIdentityKeyProblem(layer, key);
|
|
76
|
+
if (problem !== null) throw new InvalidIdentityKeyError(layer, key, problem);
|
|
77
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// Identity read primitives — the port of Store.get_all_identity,
|
|
2
|
+
// get_identity_by_layer, and get_identity (read side only; the write side
|
|
3
|
+
// lives in identityStore.ts / setIdentity.ts).
|
|
4
|
+
|
|
5
|
+
import type { Database } from "#db/database.ts";
|
|
6
|
+
import { optionalString, requireString } from "#db/rowDecode.ts";
|
|
7
|
+
|
|
8
|
+
/** The projection `identity list` needs: layer, key, and content for the preview. */
|
|
9
|
+
export interface IdentityListRow {
|
|
10
|
+
layer: string;
|
|
11
|
+
key: string;
|
|
12
|
+
content: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function toListRow(row: Record<string, unknown>): IdentityListRow {
|
|
16
|
+
return {
|
|
17
|
+
layer: requireString(row, "layer"),
|
|
18
|
+
key: requireString(row, "key"),
|
|
19
|
+
content: requireString(row, "content"),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Port of `get_all_identity`: every identity row, ordered by (layer, key). */
|
|
24
|
+
export function listAllIdentity(db: Database): IdentityListRow[] {
|
|
25
|
+
return db
|
|
26
|
+
.prepare("SELECT layer, key, content FROM identity ORDER BY layer, key")
|
|
27
|
+
.all()
|
|
28
|
+
.map(toListRow);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Port of `get_identity_by_layer`: rows for one layer, ordered by key. */
|
|
32
|
+
export function listIdentityByLayer(db: Database, layer: string): IdentityListRow[] {
|
|
33
|
+
return db
|
|
34
|
+
.prepare("SELECT layer, key, content FROM identity WHERE layer = ? ORDER BY key")
|
|
35
|
+
.all(layer)
|
|
36
|
+
.map(toListRow);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Port of `get_identity` (content only): null when the (layer, key) row is absent. */
|
|
40
|
+
export function getIdentityContent(db: Database, layer: string, key: string): string | null {
|
|
41
|
+
const row = db
|
|
42
|
+
.prepare("SELECT content FROM identity WHERE layer = ? AND key = ?")
|
|
43
|
+
.get(layer, key);
|
|
44
|
+
if (row === undefined) return null;
|
|
45
|
+
return requireString(row, "content");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Raw metadata column for one (layer, key): null when the row or its metadata is absent. */
|
|
49
|
+
export function getIdentityMetadata(db: Database, layer: string, key: string): string | null {
|
|
50
|
+
const row = db
|
|
51
|
+
.prepare("SELECT metadata FROM identity WHERE layer = ? AND key = ?")
|
|
52
|
+
.get(layer, key);
|
|
53
|
+
if (row === undefined) return null;
|
|
54
|
+
return optionalString(row, "metadata");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Whether an identity row exists for (layer, key) — an existence check
|
|
59
|
+
* independent of content truthiness. Needed because Python's `existing and not
|
|
60
|
+
* force` checks object truthiness (an `Identity` row is always truthy, even
|
|
61
|
+
* with empty content); reusing `getIdentityContent(...)` for this would
|
|
62
|
+
* misread an existing-but-empty-content row as absent.
|
|
63
|
+
*/
|
|
64
|
+
export function identityRowExists(db: Database, layer: string, key: string): boolean {
|
|
65
|
+
return (
|
|
66
|
+
db.prepare("SELECT 1 FROM identity WHERE layer = ? AND key = ? LIMIT 1").get(layer, key) !==
|
|
67
|
+
undefined
|
|
68
|
+
);
|
|
69
|
+
}
|