@cmetech/otto 1.1.1 → 1.2.5
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/dist/coworker/persona-commands.d.ts +1 -0
- package/dist/coworker/persona-commands.js +5 -0
- package/dist/coworker/persona-commands.test.d.ts +1 -0
- package/dist/coworker/persona-commands.test.js +45 -0
- package/dist/resources/.managed-resources-content-hash +1 -1
- package/dist/resources/extensions/coworker-artifacts/artifacts-command.js +31 -0
- package/dist/resources/extensions/coworker-artifacts/artifacts-singleton.js +17 -0
- package/dist/resources/extensions/coworker-artifacts/extension-manifest.json +13 -0
- package/dist/resources/extensions/coworker-artifacts/index.js +125 -0
- package/dist/resources/extensions/coworker-artifacts/list-tool.js +27 -0
- package/dist/resources/extensions/coworker-artifacts/open-tool.js +25 -0
- package/dist/resources/extensions/coworker-memory/extension-manifest.json +13 -0
- package/dist/resources/extensions/coworker-memory/index.js +219 -0
- package/dist/resources/extensions/coworker-memory/memorize-tool.js +10 -0
- package/dist/resources/extensions/coworker-memory/memory-command.js +157 -0
- package/dist/resources/extensions/coworker-memory/memory-singleton.js +55 -0
- package/dist/resources/extensions/coworker-memory/recall-tool.js +18 -0
- package/dist/resources/extensions/coworker-memory/session-hooks.js +45 -0
- package/dist/resources/extensions/coworker-scratchpad/attach-banners.js +53 -0
- package/dist/resources/extensions/coworker-scratchpad/extension-manifest.json +13 -0
- package/dist/resources/extensions/coworker-scratchpad/format-age.js +9 -0
- package/dist/resources/extensions/coworker-scratchpad/helpers.js +38 -0
- package/dist/resources/extensions/coworker-scratchpad/index.js +199 -0
- package/dist/resources/extensions/coworker-scratchpad/mime-bundle.js +20 -0
- package/dist/resources/extensions/coworker-scratchpad/scratchpad-tool.js +118 -0
- package/dist/resources/extensions/coworker-scratchpad/session-sidecar.js +60 -0
- package/dist/resources/extensions/coworker-scratchpad/sp-command.js +597 -0
- package/dist/resources/extensions/coworker-scratchpad/workspace-pointer.js +41 -0
- package/dist/resources/extensions/coworker-scratchpad/workspace-root.js +17 -0
- package/dist/resources/extensions/coworker-vault/audit-command.js +35 -0
- package/dist/resources/extensions/coworker-vault/connect-command.js +42 -0
- package/dist/resources/extensions/coworker-vault/datasource-command.js +50 -0
- package/dist/resources/extensions/coworker-vault/extension-manifest.json +12 -0
- package/dist/resources/extensions/coworker-vault/index.js +171 -0
- package/dist/resources/extensions/coworker-vault/test-helpers.js +86 -0
- package/dist/resources/extensions/coworker-vault/vault-singleton.js +24 -0
- package/dist/resources/extensions/otto/commands/release-notes/_data.js +83 -0
- package/dist/resources/extensions/otto/commands/release-notes/command.js +15 -4
- package/dist/resources/extensions/otto/index.js +31 -6
- package/dist/resources/extensions/shared/coworker-paths.js +8 -0
- package/dist/resources/extensions/slash-commands/{audit.js → audit-codebase.js} +4 -4
- package/dist/resources/extensions/slash-commands/extension-manifest.json +1 -1
- package/dist/resources/extensions/slash-commands/index.js +2 -2
- package/dist/resources/extensions/subagent/index.js +8 -1
- package/dist/resources/extensions/subagent/launch.js +37 -5
- package/dist/resources/extensions/subagent/run-store.js +1 -0
- package/dist/resources/extensions/workflow/bootstrap/register-extension.js +2 -0
- package/dist/resources/extensions/workflow/bootstrap/register-hooks.js +10 -0
- package/dist/resources/extensions/workflow/health-widget-core.js +1 -1
- package/dist/resources/extensions/workflow/persona-status.js +87 -0
- package/package.json +26 -10
- package/packages/contracts/package.json +1 -1
- package/packages/coworker-artifacts/dist/artifact-store.d.ts +25 -0
- package/packages/coworker-artifacts/dist/artifact-store.js +187 -0
- package/packages/coworker-artifacts/dist/dir-snapshot.d.ts +7 -0
- package/packages/coworker-artifacts/dist/dir-snapshot.js +54 -0
- package/packages/coworker-artifacts/dist/errors.d.ts +18 -0
- package/packages/coworker-artifacts/dist/errors.js +37 -0
- package/packages/coworker-artifacts/dist/index.d.ts +7 -0
- package/packages/coworker-artifacts/dist/index.js +7 -0
- package/packages/coworker-artifacts/dist/readme-renderer.d.ts +5 -0
- package/packages/coworker-artifacts/dist/readme-renderer.js +47 -0
- package/packages/coworker-artifacts/dist/resolve-uri.d.ts +3 -0
- package/packages/coworker-artifacts/dist/resolve-uri.js +29 -0
- package/packages/coworker-artifacts/dist/slug.d.ts +4 -0
- package/packages/coworker-artifacts/dist/slug.js +32 -0
- package/packages/coworker-artifacts/dist/types.d.ts +52 -0
- package/packages/coworker-artifacts/dist/types.js +1 -0
- package/packages/coworker-artifacts/package.json +20 -0
- package/packages/coworker-artifacts/src/artifact-store.test.ts +188 -0
- package/packages/coworker-artifacts/src/artifact-store.ts +206 -0
- package/packages/coworker-artifacts/src/artifacts-integration.test.ts +109 -0
- package/packages/coworker-artifacts/src/dir-snapshot.test.ts +71 -0
- package/packages/coworker-artifacts/src/dir-snapshot.ts +52 -0
- package/packages/coworker-artifacts/src/errors.test.ts +37 -0
- package/packages/coworker-artifacts/src/errors.ts +28 -0
- package/packages/coworker-artifacts/src/index.test.ts +22 -0
- package/packages/coworker-artifacts/src/index.ts +7 -0
- package/packages/coworker-artifacts/src/readme-renderer.test.ts +72 -0
- package/packages/coworker-artifacts/src/readme-renderer.ts +56 -0
- package/packages/coworker-artifacts/src/resolve-uri.test.ts +46 -0
- package/packages/coworker-artifacts/src/resolve-uri.ts +29 -0
- package/packages/coworker-artifacts/src/slug.test.ts +47 -0
- package/packages/coworker-artifacts/src/slug.ts +31 -0
- package/packages/coworker-artifacts/src/types.ts +61 -0
- package/packages/coworker-artifacts/tsconfig.json +15 -0
- package/packages/coworker-artifacts/tsconfig.publish.json +4 -0
- package/packages/coworker-memory/dist/context-injection.d.ts +9 -0
- package/packages/coworker-memory/dist/context-injection.js +41 -0
- package/packages/coworker-memory/dist/errors.d.ts +25 -0
- package/packages/coworker-memory/dist/errors.js +51 -0
- package/packages/coworker-memory/dist/index.d.ts +12 -0
- package/packages/coworker-memory/dist/index.js +12 -0
- package/packages/coworker-memory/dist/layer-a-store.d.ts +16 -0
- package/packages/coworker-memory/dist/layer-a-store.js +78 -0
- package/packages/coworker-memory/dist/local-sqlite-backend.d.ts +28 -0
- package/packages/coworker-memory/dist/local-sqlite-backend.js +167 -0
- package/packages/coworker-memory/dist/memory-backend.d.ts +14 -0
- package/packages/coworker-memory/dist/memory-backend.js +1 -0
- package/packages/coworker-memory/dist/memory-recorder.d.ts +50 -0
- package/packages/coworker-memory/dist/memory-recorder.js +69 -0
- package/packages/coworker-memory/dist/migrations/001-init.sql +38 -0
- package/packages/coworker-memory/dist/migrations/002-artifact-kind.sql +50 -0
- package/packages/coworker-memory/dist/paste-detector.d.ts +5 -0
- package/packages/coworker-memory/dist/paste-detector.js +14 -0
- package/packages/coworker-memory/dist/persona-seed.d.ts +10 -0
- package/packages/coworker-memory/dist/persona-seed.js +38 -0
- package/packages/coworker-memory/dist/recall-formatter.d.ts +2 -0
- package/packages/coworker-memory/dist/recall-formatter.js +14 -0
- package/packages/coworker-memory/dist/scope-resolver.d.ts +9 -0
- package/packages/coworker-memory/dist/scope-resolver.js +10 -0
- package/packages/coworker-memory/dist/types.d.ts +51 -0
- package/packages/coworker-memory/dist/types.js +2 -0
- package/packages/coworker-memory/dist/workspace-id.d.ts +3 -0
- package/packages/coworker-memory/dist/workspace-id.js +54 -0
- package/packages/coworker-memory/package.json +35 -0
- package/packages/coworker-memory/src/activator-integration.test.ts +141 -0
- package/packages/coworker-memory/src/context-injection.test.ts +72 -0
- package/packages/coworker-memory/src/context-injection.ts +57 -0
- package/packages/coworker-memory/src/errors.test.ts +45 -0
- package/packages/coworker-memory/src/errors.ts +42 -0
- package/packages/coworker-memory/src/index.test.ts +21 -0
- package/packages/coworker-memory/src/index.ts +12 -0
- package/packages/coworker-memory/src/layer-a-store.test.ts +85 -0
- package/packages/coworker-memory/src/layer-a-store.ts +88 -0
- package/packages/coworker-memory/src/local-sqlite-backend.test.ts +110 -0
- package/packages/coworker-memory/src/local-sqlite-backend.ts +185 -0
- package/packages/coworker-memory/src/memory-backend.ts +10 -0
- package/packages/coworker-memory/src/memory-integration.test.ts +89 -0
- package/packages/coworker-memory/src/memory-recorder.test.ts +101 -0
- package/packages/coworker-memory/src/memory-recorder.ts +95 -0
- package/packages/coworker-memory/src/migrations/001-init.sql +38 -0
- package/packages/coworker-memory/src/migrations/002-artifact-kind.sql +50 -0
- package/packages/coworker-memory/src/paste-detector.test.ts +23 -0
- package/packages/coworker-memory/src/paste-detector.ts +18 -0
- package/packages/coworker-memory/src/persona-seed.test.ts +57 -0
- package/packages/coworker-memory/src/persona-seed.ts +46 -0
- package/packages/coworker-memory/src/recall-formatter.test.ts +34 -0
- package/packages/coworker-memory/src/recall-formatter.ts +15 -0
- package/packages/coworker-memory/src/scope-resolver.test.ts +23 -0
- package/packages/coworker-memory/src/scope-resolver.ts +18 -0
- package/packages/coworker-memory/src/types.ts +61 -0
- package/packages/coworker-memory/src/workspace-id.test.ts +48 -0
- package/packages/coworker-memory/src/workspace-id.ts +56 -0
- package/packages/coworker-memory/tsconfig.json +15 -0
- package/packages/coworker-memory/tsconfig.publish.json +4 -0
- package/packages/coworker-persona/dist/commands.d.ts +7 -0
- package/packages/coworker-persona/dist/commands.js +35 -0
- package/packages/coworker-persona/dist/defaults/manifest.yaml +12 -0
- package/packages/coworker-persona/dist/defaults/steering/identity.md +3 -0
- package/packages/coworker-persona/dist/index.d.ts +3 -0
- package/packages/coworker-persona/dist/index.js +3 -0
- package/packages/coworker-persona/dist/manifest.d.ts +24 -0
- package/packages/coworker-persona/dist/manifest.js +21 -0
- package/packages/coworker-persona/dist/registry.d.ts +22 -0
- package/packages/coworker-persona/dist/registry.js +142 -0
- package/packages/coworker-persona/package.json +28 -0
- package/packages/coworker-persona/scripts/copy-defaults.cjs +17 -0
- package/packages/coworker-persona/src/commands.ts +47 -0
- package/packages/coworker-persona/src/defaults/manifest.yaml +12 -0
- package/packages/coworker-persona/src/defaults/steering/identity.md +3 -0
- package/packages/coworker-persona/src/index.ts +3 -0
- package/packages/coworker-persona/src/manifest.test.ts +67 -0
- package/packages/coworker-persona/src/manifest.ts +49 -0
- package/packages/coworker-persona/src/registry.test.ts +89 -0
- package/packages/coworker-persona/src/registry.ts +147 -0
- package/packages/coworker-persona/tsconfig.json +15 -0
- package/packages/coworker-persona/tsconfig.publish.json +4 -0
- package/packages/coworker-scratchpad/dist/cell-archive.d.ts +39 -0
- package/packages/coworker-scratchpad/dist/cell-archive.js +77 -0
- package/packages/coworker-scratchpad/dist/cell-tree.d.ts +14 -0
- package/packages/coworker-scratchpad/dist/cell-tree.js +72 -0
- package/packages/coworker-scratchpad/dist/child-process-runtime.d.ts +129 -0
- package/packages/coworker-scratchpad/dist/child-process-runtime.js +427 -0
- package/packages/coworker-scratchpad/dist/collector-registry.d.ts +12 -0
- package/packages/coworker-scratchpad/dist/collector-registry.js +29 -0
- package/packages/coworker-scratchpad/dist/detect-kind.d.ts +3 -0
- package/packages/coworker-scratchpad/dist/detect-kind.js +19 -0
- package/packages/coworker-scratchpad/dist/file-collector.d.ts +15 -0
- package/packages/coworker-scratchpad/dist/file-collector.js +99 -0
- package/packages/coworker-scratchpad/dist/index.d.ts +13 -0
- package/packages/coworker-scratchpad/dist/index.js +13 -0
- package/packages/coworker-scratchpad/dist/kernel-bindings.d.ts +49 -0
- package/packages/coworker-scratchpad/dist/kernel-bindings.js +220 -0
- package/packages/coworker-scratchpad/dist/kernel-entry.d.ts +1 -0
- package/packages/coworker-scratchpad/dist/kernel-entry.js +355 -0
- package/packages/coworker-scratchpad/dist/kernel-protocol.d.ts +171 -0
- package/packages/coworker-scratchpad/dist/kernel-protocol.js +48 -0
- package/packages/coworker-scratchpad/dist/kernel-spawn.d.ts +3 -0
- package/packages/coworker-scratchpad/dist/kernel-spawn.js +54 -0
- package/packages/coworker-scratchpad/dist/namespace-codec.d.ts +22 -0
- package/packages/coworker-scratchpad/dist/namespace-codec.js +61 -0
- package/packages/coworker-scratchpad/dist/scratchpad-lock.d.ts +24 -0
- package/packages/coworker-scratchpad/dist/scratchpad-lock.js +86 -0
- package/packages/coworker-scratchpad/dist/scratchpad-manager.d.ts +193 -0
- package/packages/coworker-scratchpad/dist/scratchpad-manager.js +866 -0
- package/packages/coworker-scratchpad/dist/staleness-banner.d.ts +12 -0
- package/packages/coworker-scratchpad/dist/staleness-banner.js +27 -0
- package/packages/coworker-scratchpad/package.json +31 -0
- package/packages/coworker-scratchpad/src/cell-archive.test.ts +150 -0
- package/packages/coworker-scratchpad/src/cell-archive.ts +97 -0
- package/packages/coworker-scratchpad/src/cell-tree.test.ts +105 -0
- package/packages/coworker-scratchpad/src/cell-tree.ts +90 -0
- package/packages/coworker-scratchpad/src/child-process-runtime.test.ts +413 -0
- package/packages/coworker-scratchpad/src/child-process-runtime.ts +493 -0
- package/packages/coworker-scratchpad/src/collector-registry.test.ts +69 -0
- package/packages/coworker-scratchpad/src/collector-registry.ts +33 -0
- package/packages/coworker-scratchpad/src/detect-kind.test.ts +33 -0
- package/packages/coworker-scratchpad/src/detect-kind.ts +22 -0
- package/packages/coworker-scratchpad/src/file-collector.test.ts +109 -0
- package/packages/coworker-scratchpad/src/file-collector.ts +114 -0
- package/packages/coworker-scratchpad/src/index.ts +74 -0
- package/packages/coworker-scratchpad/src/kernel-bindings.test.ts +188 -0
- package/packages/coworker-scratchpad/src/kernel-bindings.ts +279 -0
- package/packages/coworker-scratchpad/src/kernel-entry.test.ts +123 -0
- package/packages/coworker-scratchpad/src/kernel-entry.ts +390 -0
- package/packages/coworker-scratchpad/src/kernel-protocol.test.ts +105 -0
- package/packages/coworker-scratchpad/src/kernel-protocol.ts +230 -0
- package/packages/coworker-scratchpad/src/kernel-spawn.test.ts +60 -0
- package/packages/coworker-scratchpad/src/kernel-spawn.ts +54 -0
- package/packages/coworker-scratchpad/src/namespace-codec.test.ts +102 -0
- package/packages/coworker-scratchpad/src/namespace-codec.ts +90 -0
- package/packages/coworker-scratchpad/src/scratchpad-lock.test.ts +98 -0
- package/packages/coworker-scratchpad/src/scratchpad-lock.ts +102 -0
- package/packages/coworker-scratchpad/src/scratchpad-manager.test.ts +1343 -0
- package/packages/coworker-scratchpad/src/scratchpad-manager.ts +891 -0
- package/packages/coworker-scratchpad/src/staleness-banner.test.ts +53 -0
- package/packages/coworker-scratchpad/src/staleness-banner.ts +33 -0
- package/packages/coworker-scratchpad/src/vault-integration.test.ts +221 -0
- package/packages/coworker-scratchpad/tsconfig.json +15 -0
- package/packages/coworker-scratchpad/tsconfig.publish.json +4 -0
- package/packages/coworker-types/dist/artifacts.d.ts +31 -0
- package/packages/coworker-types/dist/artifacts.js +2 -0
- package/packages/coworker-types/dist/contracts.d.ts +32 -0
- package/packages/coworker-types/dist/contracts.js +1 -0
- package/packages/coworker-types/dist/index.d.ts +5 -0
- package/packages/coworker-types/dist/index.js +5 -0
- package/packages/coworker-types/dist/memory.d.ts +61 -0
- package/packages/coworker-types/dist/memory.js +3 -0
- package/packages/coworker-types/dist/scratchpad.d.ts +43 -0
- package/packages/coworker-types/dist/scratchpad.js +2 -0
- package/packages/coworker-types/dist/vault.d.ts +34 -0
- package/packages/coworker-types/dist/vault.js +2 -0
- package/packages/coworker-types/package.json +24 -0
- package/packages/coworker-types/src/artifacts.test.ts +52 -0
- package/packages/coworker-types/src/artifacts.ts +35 -0
- package/packages/coworker-types/src/contracts.test.ts +43 -0
- package/packages/coworker-types/src/contracts.ts +36 -0
- package/packages/coworker-types/src/index.ts +5 -0
- package/packages/coworker-types/src/memory.test.ts +50 -0
- package/packages/coworker-types/src/memory.ts +79 -0
- package/packages/coworker-types/src/scratchpad.test.ts +46 -0
- package/packages/coworker-types/src/scratchpad.ts +51 -0
- package/packages/coworker-types/src/smoke.test.ts +34 -0
- package/packages/coworker-types/src/vault.test.ts +49 -0
- package/packages/coworker-types/src/vault.ts +40 -0
- package/packages/coworker-types/tsconfig.json +15 -0
- package/packages/coworker-types/tsconfig.publish.json +4 -0
- package/packages/coworker-utils/dist/audit-log.d.ts +34 -0
- package/packages/coworker-utils/dist/audit-log.js +88 -0
- package/packages/coworker-utils/dist/index.d.ts +6 -0
- package/packages/coworker-utils/dist/index.js +6 -0
- package/packages/coworker-utils/dist/lease.d.ts +7 -0
- package/packages/coworker-utils/dist/lease.js +67 -0
- package/packages/coworker-utils/dist/logger.d.ts +13 -0
- package/packages/coworker-utils/dist/logger.js +26 -0
- package/packages/coworker-utils/dist/migration-runner.d.ts +7 -0
- package/packages/coworker-utils/dist/migration-runner.js +36 -0
- package/packages/coworker-utils/dist/ndjson-channel.d.ts +3 -0
- package/packages/coworker-utils/dist/ndjson-channel.js +38 -0
- package/packages/coworker-utils/dist/secret-scanner.d.ts +10 -0
- package/packages/coworker-utils/dist/secret-scanner.js +42 -0
- package/packages/coworker-utils/package.json +24 -0
- package/packages/coworker-utils/src/audit-log.test.ts +140 -0
- package/packages/coworker-utils/src/audit-log.ts +107 -0
- package/packages/coworker-utils/src/index.ts +6 -0
- package/packages/coworker-utils/src/lease.test.ts +64 -0
- package/packages/coworker-utils/src/lease.ts +76 -0
- package/packages/coworker-utils/src/logger.test.ts +50 -0
- package/packages/coworker-utils/src/logger.ts +45 -0
- package/packages/coworker-utils/src/migration-runner.test.ts +65 -0
- package/packages/coworker-utils/src/migration-runner.ts +50 -0
- package/packages/coworker-utils/src/ndjson-channel.test.ts +76 -0
- package/packages/coworker-utils/src/ndjson-channel.ts +41 -0
- package/packages/coworker-utils/src/secret-scanner.test.ts +61 -0
- package/packages/coworker-utils/src/secret-scanner.ts +56 -0
- package/packages/coworker-utils/tsconfig.json +15 -0
- package/packages/coworker-utils/tsconfig.publish.json +4 -0
- package/packages/coworker-vault/dist/data-vault.d.ts +41 -0
- package/packages/coworker-vault/dist/data-vault.js +223 -0
- package/packages/coworker-vault/dist/engine-registry.d.ts +34 -0
- package/packages/coworker-vault/dist/engine-registry.js +90 -0
- package/packages/coworker-vault/dist/engines/jira.yaml +17 -0
- package/packages/coworker-vault/dist/errors.d.ts +28 -0
- package/packages/coworker-vault/dist/errors.js +57 -0
- package/packages/coworker-vault/dist/index.d.ts +6 -0
- package/packages/coworker-vault/dist/index.js +6 -0
- package/packages/coworker-vault/dist/injector.d.ts +19 -0
- package/packages/coworker-vault/dist/injector.js +77 -0
- package/packages/coworker-vault/dist/types.d.ts +28 -0
- package/packages/coworker-vault/dist/types.js +1 -0
- package/packages/coworker-vault/dist/vault-keep.d.ts +4 -0
- package/packages/coworker-vault/dist/vault-keep.js +21 -0
- package/packages/coworker-vault/package.json +29 -0
- package/packages/coworker-vault/src/data-vault.test.ts +199 -0
- package/packages/coworker-vault/src/data-vault.ts +257 -0
- package/packages/coworker-vault/src/engine-registry.test.ts +120 -0
- package/packages/coworker-vault/src/engine-registry.ts +107 -0
- package/packages/coworker-vault/src/engines/jira.yaml +17 -0
- package/packages/coworker-vault/src/errors.test.ts +58 -0
- package/packages/coworker-vault/src/errors.ts +50 -0
- package/packages/coworker-vault/src/index.test.ts +24 -0
- package/packages/coworker-vault/src/index.ts +6 -0
- package/packages/coworker-vault/src/injector.test.ts +109 -0
- package/packages/coworker-vault/src/injector.ts +98 -0
- package/packages/coworker-vault/src/types.ts +33 -0
- package/packages/coworker-vault/src/vault-keep.test.ts +49 -0
- package/packages/coworker-vault/src/vault-keep.ts +31 -0
- package/packages/coworker-vault/tsconfig.json +15 -0
- package/packages/coworker-vault/tsconfig.publish.json +4 -0
- package/packages/daemon/package.json +3 -3
- package/packages/mcp-server/package.json +3 -3
- package/packages/mcp-server/tsconfig.tsbuildinfo +1 -1
- package/packages/native/package.json +1 -1
- package/packages/native/tsconfig.tsbuildinfo +1 -1
- package/packages/pi-agent-core/package.json +1 -1
- package/packages/pi-agent-core/tsconfig.tsbuildinfo +1 -1
- package/packages/pi-ai/package.json +1 -1
- package/packages/pi-ai/tsconfig.tsbuildinfo +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts +6 -1
- package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/runner.js +22 -3
- package/packages/pi-coding-agent/dist/core/extensions/runner.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/resolve-config-value.test.js +11 -0
- package/packages/pi-coding-agent/dist/core/resolve-config-value.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/rpc/raw-stdout.d.ts +47 -0
- package/packages/pi-coding-agent/dist/modes/rpc/raw-stdout.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/modes/rpc/raw-stdout.js +107 -0
- package/packages/pi-coding-agent/dist/modes/rpc/raw-stdout.js.map +1 -0
- package/packages/pi-coding-agent/dist/modes/rpc/raw-stdout.regression.test.d.ts +19 -0
- package/packages/pi-coding-agent/dist/modes/rpc/raw-stdout.regression.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/modes/rpc/raw-stdout.regression.test.js +121 -0
- package/packages/pi-coding-agent/dist/modes/rpc/raw-stdout.regression.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/rpc/rpc-mode.js +17 -1
- package/packages/pi-coding-agent/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/packages/pi-coding-agent/package.json +2 -2
- package/packages/pi-coding-agent/src/core/extensions/runner.ts +22 -3
- package/packages/pi-coding-agent/src/core/resolve-config-value.test.ts +11 -0
- package/packages/pi-coding-agent/src/modes/rpc/raw-stdout.regression.test.ts +129 -0
- package/packages/pi-coding-agent/src/modes/rpc/raw-stdout.ts +117 -0
- package/packages/pi-coding-agent/src/modes/rpc/rpc-mode.ts +18 -1
- package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
- package/packages/pi-tui/package.json +1 -1
- package/packages/pi-tui/tsconfig.tsbuildinfo +1 -1
- package/packages/rpc-client/package.json +2 -2
- package/packages/rpc-client/tsconfig.tsbuildinfo +1 -1
- package/pkg/package.json +1 -1
- package/scripts/install.js +6 -5
- package/src/resources/extensions/coworker-artifacts/artifacts-command.test.ts +54 -0
- package/src/resources/extensions/coworker-artifacts/artifacts-command.ts +43 -0
- package/src/resources/extensions/coworker-artifacts/artifacts-singleton.test.ts +25 -0
- package/src/resources/extensions/coworker-artifacts/artifacts-singleton.ts +29 -0
- package/src/resources/extensions/coworker-artifacts/extension-manifest.json +13 -0
- package/src/resources/extensions/coworker-artifacts/index.test.ts +46 -0
- package/src/resources/extensions/coworker-artifacts/index.ts +154 -0
- package/src/resources/extensions/coworker-artifacts/list-tool.test.ts +29 -0
- package/src/resources/extensions/coworker-artifacts/list-tool.ts +53 -0
- package/src/resources/extensions/coworker-artifacts/open-tool.test.ts +30 -0
- package/src/resources/extensions/coworker-artifacts/open-tool.ts +43 -0
- package/src/resources/extensions/coworker-memory/extension-manifest.json +13 -0
- package/src/resources/extensions/coworker-memory/index.test.ts +137 -0
- package/src/resources/extensions/coworker-memory/index.ts +257 -0
- package/src/resources/extensions/coworker-memory/memorize-tool.test.ts +41 -0
- package/src/resources/extensions/coworker-memory/memorize-tool.ts +20 -0
- package/src/resources/extensions/coworker-memory/memory-command.test.ts +134 -0
- package/src/resources/extensions/coworker-memory/memory-command.ts +131 -0
- package/src/resources/extensions/coworker-memory/memory-singleton.test.ts +41 -0
- package/src/resources/extensions/coworker-memory/memory-singleton.ts +89 -0
- package/src/resources/extensions/coworker-memory/recall-tool.test.ts +50 -0
- package/src/resources/extensions/coworker-memory/recall-tool.ts +35 -0
- package/src/resources/extensions/coworker-memory/session-hooks.test.ts +77 -0
- package/src/resources/extensions/coworker-memory/session-hooks.ts +61 -0
- package/src/resources/extensions/coworker-scratchpad/attach-banners.test.ts +124 -0
- package/src/resources/extensions/coworker-scratchpad/attach-banners.ts +67 -0
- package/src/resources/extensions/coworker-scratchpad/extension-manifest.json +13 -0
- package/src/resources/extensions/coworker-scratchpad/format-age.test.ts +30 -0
- package/src/resources/extensions/coworker-scratchpad/format-age.ts +6 -0
- package/src/resources/extensions/coworker-scratchpad/helpers.test.ts +93 -0
- package/src/resources/extensions/coworker-scratchpad/helpers.ts +42 -0
- package/src/resources/extensions/coworker-scratchpad/index.test.ts +514 -0
- package/src/resources/extensions/coworker-scratchpad/index.ts +207 -0
- package/src/resources/extensions/coworker-scratchpad/mime-bundle.test.ts +61 -0
- package/src/resources/extensions/coworker-scratchpad/mime-bundle.ts +23 -0
- package/src/resources/extensions/coworker-scratchpad/scratchpad-tool.test.ts +137 -0
- package/src/resources/extensions/coworker-scratchpad/scratchpad-tool.ts +165 -0
- package/src/resources/extensions/coworker-scratchpad/session-sidecar.test.ts +133 -0
- package/src/resources/extensions/coworker-scratchpad/session-sidecar.ts +68 -0
- package/src/resources/extensions/coworker-scratchpad/sp-command.test.ts +836 -0
- package/src/resources/extensions/coworker-scratchpad/sp-command.ts +602 -0
- package/src/resources/extensions/coworker-scratchpad/workspace-pointer.test.ts +74 -0
- package/src/resources/extensions/coworker-scratchpad/workspace-pointer.ts +55 -0
- package/src/resources/extensions/coworker-scratchpad/workspace-root.test.ts +51 -0
- package/src/resources/extensions/coworker-scratchpad/workspace-root.ts +16 -0
- package/src/resources/extensions/coworker-vault/audit-command.test.ts +109 -0
- package/src/resources/extensions/coworker-vault/audit-command.ts +56 -0
- package/src/resources/extensions/coworker-vault/connect-command.test.ts +103 -0
- package/src/resources/extensions/coworker-vault/connect-command.ts +69 -0
- package/src/resources/extensions/coworker-vault/datasource-command.test.ts +80 -0
- package/src/resources/extensions/coworker-vault/datasource-command.ts +81 -0
- package/src/resources/extensions/coworker-vault/extension-manifest.json +12 -0
- package/src/resources/extensions/coworker-vault/index.test.ts +82 -0
- package/src/resources/extensions/coworker-vault/index.ts +181 -0
- package/src/resources/extensions/coworker-vault/test-helpers.ts +120 -0
- package/src/resources/extensions/coworker-vault/vault-singleton.test.ts +27 -0
- package/src/resources/extensions/coworker-vault/vault-singleton.ts +40 -0
- package/src/resources/extensions/otto/commands/release-notes/_data.ts +97 -0
- package/src/resources/extensions/otto/commands/release-notes/command.ts +16 -3
- package/src/resources/extensions/otto/index.ts +29 -6
- package/src/resources/extensions/shared/coworker-paths.test.ts +40 -0
- package/src/resources/extensions/shared/coworker-paths.ts +10 -0
- package/src/resources/extensions/slash-commands/{audit.ts → audit-codebase.ts} +4 -4
- package/src/resources/extensions/slash-commands/extension-manifest.json +1 -1
- package/src/resources/extensions/slash-commands/index.ts +2 -2
- package/src/resources/extensions/subagent/index.ts +9 -0
- package/src/resources/extensions/subagent/launch.test.ts +97 -0
- package/src/resources/extensions/subagent/launch.ts +42 -5
- package/src/resources/extensions/subagent/run-store.ts +3 -1
- package/src/resources/extensions/workflow/bootstrap/register-extension.ts +2 -0
- package/src/resources/extensions/workflow/bootstrap/register-hooks.ts +10 -0
- package/src/resources/extensions/workflow/health-widget-core.ts +1 -1
- package/src/resources/extensions/workflow/persona-status.ts +109 -0
- package/src/resources/extensions/workflow/tests/auto-recovery.test.ts +34 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// packages/coworker-memory/src/workspace-id.ts
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync, chmodSync } from 'node:fs';
|
|
3
|
+
import { basename, join } from 'node:path';
|
|
4
|
+
import { createHash } from 'node:crypto';
|
|
5
|
+
function pathHash6(absolutePath) {
|
|
6
|
+
return createHash('sha256').update(absolutePath).digest('hex').slice(0, 6);
|
|
7
|
+
}
|
|
8
|
+
function deriveSlug(workspaceDir) {
|
|
9
|
+
let base = basename(workspaceDir).replace(/[^a-z0-9-]/gi, '-').replace(/^-+|-+$/g, '').toLowerCase();
|
|
10
|
+
if (!base)
|
|
11
|
+
base = 'workspace';
|
|
12
|
+
return `${base}-${pathHash6(workspaceDir)}`;
|
|
13
|
+
}
|
|
14
|
+
export async function resolveWorkspaceId(workspaceDir) {
|
|
15
|
+
const memDir = join(workspaceDir, '.otto', 'memory');
|
|
16
|
+
const path = join(memDir, 'workspace.json');
|
|
17
|
+
mkdirSync(memDir, { recursive: true, mode: 0o700 });
|
|
18
|
+
if (existsSync(path)) {
|
|
19
|
+
try {
|
|
20
|
+
const data = JSON.parse(readFileSync(path, 'utf8'));
|
|
21
|
+
if (data && data._schema === 1 && typeof data.id === 'string' && data.id.length > 0) {
|
|
22
|
+
return data;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
catch { /* fall through to recreate */ }
|
|
26
|
+
// Move broken aside.
|
|
27
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
28
|
+
try {
|
|
29
|
+
renameSync(path, `${path}.broken-${stamp}`);
|
|
30
|
+
}
|
|
31
|
+
catch { /* best effort */ }
|
|
32
|
+
}
|
|
33
|
+
const fresh = {
|
|
34
|
+
_schema: 1,
|
|
35
|
+
id: deriveSlug(workspaceDir),
|
|
36
|
+
created_at: new Date().toISOString(),
|
|
37
|
+
memory_seed_applied: false,
|
|
38
|
+
memory_seed_persona: null,
|
|
39
|
+
};
|
|
40
|
+
const tmp = `${path}.tmp`;
|
|
41
|
+
writeFileSync(tmp, JSON.stringify(fresh, null, 2), { mode: 0o600 });
|
|
42
|
+
chmodSync(tmp, 0o600);
|
|
43
|
+
renameSync(tmp, path);
|
|
44
|
+
return fresh;
|
|
45
|
+
}
|
|
46
|
+
export async function writeWorkspaceId(workspaceDir, rec) {
|
|
47
|
+
const memDir = join(workspaceDir, '.otto', 'memory');
|
|
48
|
+
mkdirSync(memDir, { recursive: true, mode: 0o700 });
|
|
49
|
+
const path = join(memDir, 'workspace.json');
|
|
50
|
+
const tmp = `${path}.tmp`;
|
|
51
|
+
writeFileSync(tmp, JSON.stringify(rec, null, 2), { mode: 0o600 });
|
|
52
|
+
chmodSync(tmp, 0o600);
|
|
53
|
+
renameSync(tmp, path);
|
|
54
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@otto/coworker-memory",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Otto co-worker package: coworker-memory",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"otto": {
|
|
7
|
+
"linkable": true,
|
|
8
|
+
"scope": "@otto",
|
|
9
|
+
"name": "coworker-memory"
|
|
10
|
+
},
|
|
11
|
+
"main": "./dist/index.js",
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"import": "./dist/index.js"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"@otto/coworker-utils": "*",
|
|
21
|
+
"better-sqlite3": "^11.7.0",
|
|
22
|
+
"uuid": "^11.0.0",
|
|
23
|
+
"yaml": "^2.8.2"
|
|
24
|
+
},
|
|
25
|
+
"scripts": {
|
|
26
|
+
"build": "tsc -p tsconfig.json && node -e \"const fs=require('fs');const path=require('path');const src='src/migrations';const dst='dist/migrations';fs.mkdirSync(dst,{recursive:true});for(const f of fs.readdirSync(src))if(f.endsWith('.sql'))fs.copyFileSync(path.join(src,f),path.join(dst,f));\"",
|
|
27
|
+
"build:publish": "tsc -p tsconfig.publish.json && node -e \"const fs=require('fs');const path=require('path');const src='src/migrations';const dst='dist/migrations';fs.mkdirSync(dst,{recursive:true});for(const f of fs.readdirSync(src))if(f.endsWith('.sql'))fs.copyFileSync(path.join(src,f),path.join(dst,f));\""
|
|
28
|
+
},
|
|
29
|
+
"files": [
|
|
30
|
+
"dist"
|
|
31
|
+
],
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@types/better-sqlite3": "^7.6.13"
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// packages/coworker-memory/src/activator-integration.test.ts
|
|
2
|
+
import { describe, it } from 'node:test';
|
|
3
|
+
import assert from 'node:assert/strict';
|
|
4
|
+
import { mkdtempSync, mkdirSync } from 'node:fs';
|
|
5
|
+
import { tmpdir } from 'node:os';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
import type { DataLoadDrawer } from '@otto/coworker-scratchpad';
|
|
8
|
+
|
|
9
|
+
import coworkerVaultExtension from '../../../src/resources/extensions/coworker-vault/index.js';
|
|
10
|
+
import coworkerMemoryExtension, {
|
|
11
|
+
getMemoryRecorder, createMemoryBundle,
|
|
12
|
+
} from '../../../src/resources/extensions/coworker-memory/index.js';
|
|
13
|
+
import coworkerScratchpadExtension from '../../../src/resources/extensions/coworker-scratchpad/index.js';
|
|
14
|
+
import {
|
|
15
|
+
makeFakeApi, fireSessionStart, fireSessionShutdown,
|
|
16
|
+
fireBeforeAgentStart, fireAgentStart,
|
|
17
|
+
} from '../../../src/resources/extensions/coworker-vault/test-helpers.js';
|
|
18
|
+
|
|
19
|
+
describe('Phase 3.1 — cross-extension activator integration', () => {
|
|
20
|
+
it('vault + memory + scratchpad activate, recordTurn fires, recall returns drawer', async () => {
|
|
21
|
+
const global = mkdtempSync(join(tmpdir(), 'p31-g-'));
|
|
22
|
+
const ws = mkdtempSync(join(tmpdir(), 'p31-w-'));
|
|
23
|
+
const sp = mkdtempSync(join(tmpdir(), 'p31-sp-'));
|
|
24
|
+
mkdirSync(ws, { recursive: true });
|
|
25
|
+
process.env.OTTO_COWORKER_GLOBAL_DIR = global;
|
|
26
|
+
process.env.OTTO_SCRATCHPAD_ROOT = sp;
|
|
27
|
+
try {
|
|
28
|
+
// Each ext gets its own fake API in this integration shape — they don't
|
|
29
|
+
// need to share an API instance; what they share is filesystem state.
|
|
30
|
+
const vaultApi = makeFakeApi();
|
|
31
|
+
const memApi = makeFakeApi();
|
|
32
|
+
const spApi = makeFakeApi();
|
|
33
|
+
|
|
34
|
+
coworkerVaultExtension(vaultApi.api);
|
|
35
|
+
coworkerMemoryExtension(memApi.api);
|
|
36
|
+
coworkerScratchpadExtension(spApi.api);
|
|
37
|
+
|
|
38
|
+
await fireSessionStart(vaultApi, { cwd: ws });
|
|
39
|
+
await fireSessionStart(memApi, { cwd: ws });
|
|
40
|
+
await fireSessionStart(spApi, { cwd: ws });
|
|
41
|
+
|
|
42
|
+
assert.equal(vaultApi.notifyCalls.find(c => /unavailable/.test(c.message)), undefined);
|
|
43
|
+
assert.equal(memApi.notifyCalls.find(c => /unavailable/.test(c.message)), undefined);
|
|
44
|
+
assert.equal(spApi.notifyCalls.find(c => /unavailable/.test(c.message)), undefined);
|
|
45
|
+
|
|
46
|
+
assert.ok(getMemoryRecorder(), 'memory recorder live after session_start');
|
|
47
|
+
|
|
48
|
+
// before_agent_start + agent_start round-trip
|
|
49
|
+
await fireBeforeAgentStart(memApi, 'load balancer started returning 503s', 'BASE');
|
|
50
|
+
await fireAgentStart(memApi, 'sess-1', 'turn-1');
|
|
51
|
+
|
|
52
|
+
// Verify the drawer landed via a fresh read.
|
|
53
|
+
const peek = await createMemoryBundle({
|
|
54
|
+
globalDir: global, workspaceDir: ws,
|
|
55
|
+
scopeMode: 'per-project-tagged', currentScratchpadName: () => null,
|
|
56
|
+
});
|
|
57
|
+
try {
|
|
58
|
+
const r = await peek.backend.recall({ query: 'load balancer' });
|
|
59
|
+
assert.equal(r.length, 1);
|
|
60
|
+
assert.equal(r[0]!.drawer.kind, 'turn');
|
|
61
|
+
assert.match(r[0]!.drawer.content, /load balancer started returning 503s/);
|
|
62
|
+
} finally { await peek.dispose(); }
|
|
63
|
+
|
|
64
|
+
await fireSessionShutdown(memApi);
|
|
65
|
+
await fireSessionShutdown(spApi);
|
|
66
|
+
await fireSessionShutdown(vaultApi);
|
|
67
|
+
assert.equal(getMemoryRecorder(), null, 'recorder cleared');
|
|
68
|
+
} finally {
|
|
69
|
+
delete process.env.OTTO_COWORKER_GLOBAL_DIR;
|
|
70
|
+
delete process.env.OTTO_SCRATCHPAD_ROOT;
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('onDataLoad closure produces a file_load drawer when memory is live', async () => {
|
|
75
|
+
const global = mkdtempSync(join(tmpdir(), 'p31-fl-g-'));
|
|
76
|
+
const ws = mkdtempSync(join(tmpdir(), 'p31-fl-w-'));
|
|
77
|
+
const sp = mkdtempSync(join(tmpdir(), 'p31-fl-sp-'));
|
|
78
|
+
process.env.OTTO_COWORKER_GLOBAL_DIR = global;
|
|
79
|
+
process.env.OTTO_SCRATCHPAD_ROOT = sp;
|
|
80
|
+
try {
|
|
81
|
+
const memApi = makeFakeApi();
|
|
82
|
+
coworkerMemoryExtension(memApi.api);
|
|
83
|
+
await fireSessionStart(memApi, { cwd: ws });
|
|
84
|
+
const recorder = getMemoryRecorder()!;
|
|
85
|
+
assert.ok(recorder);
|
|
86
|
+
|
|
87
|
+
// Simulate the kernel emitting a data_load event by directly invoking
|
|
88
|
+
// recorder.recordFileLoad with the shape the closure would translate to.
|
|
89
|
+
// (Driving an actual kernel subprocess is overkill for this integration
|
|
90
|
+
// test; the closure logic is locked in Task 4's unit test.)
|
|
91
|
+
await recorder.recordFileLoad({
|
|
92
|
+
scratchpadName: 'p1', collector: 'file', uri: 'file:///x.csv',
|
|
93
|
+
bytes: 1024, rows_loaded: 50, turnId: '',
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const peek = await createMemoryBundle({
|
|
97
|
+
globalDir: global, workspaceDir: ws,
|
|
98
|
+
scopeMode: 'per-project-tagged', currentScratchpadName: () => null,
|
|
99
|
+
});
|
|
100
|
+
try {
|
|
101
|
+
const r = await peek.backend.recall({ query: 'file', kind: 'file_load' });
|
|
102
|
+
assert.equal(r.length, 1);
|
|
103
|
+
const parsed = JSON.parse(r[0]!.drawer.content);
|
|
104
|
+
assert.equal(parsed.collector, 'file');
|
|
105
|
+
assert.equal(parsed.uri, 'file:///x.csv');
|
|
106
|
+
assert.equal(parsed.rows_loaded, 50);
|
|
107
|
+
assert.equal(r[0]!.drawer.room, 'p1');
|
|
108
|
+
} finally { await peek.dispose(); }
|
|
109
|
+
|
|
110
|
+
await fireSessionShutdown(memApi);
|
|
111
|
+
} finally {
|
|
112
|
+
delete process.env.OTTO_COWORKER_GLOBAL_DIR;
|
|
113
|
+
delete process.env.OTTO_SCRATCHPAD_ROOT;
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('memory init failure does not break vault or scratchpad', async () => {
|
|
118
|
+
// Point only memory at a bad path; vault + scratchpad get good paths.
|
|
119
|
+
const goodGlobal = mkdtempSync(join(tmpdir(), 'p31-mix-'));
|
|
120
|
+
const ws = mkdtempSync(join(tmpdir(), 'p31-mix-ws-'));
|
|
121
|
+
const sp = mkdtempSync(join(tmpdir(), 'p31-mix-sp-'));
|
|
122
|
+
process.env.OTTO_COWORKER_GLOBAL_DIR = goodGlobal;
|
|
123
|
+
process.env.OTTO_SCRATCHPAD_ROOT = sp;
|
|
124
|
+
try {
|
|
125
|
+
const vaultApi = makeFakeApi();
|
|
126
|
+
const spApi = makeFakeApi();
|
|
127
|
+
coworkerVaultExtension(vaultApi.api);
|
|
128
|
+
coworkerScratchpadExtension(spApi.api);
|
|
129
|
+
await fireSessionStart(vaultApi, { cwd: ws });
|
|
130
|
+
await fireSessionStart(spApi, { cwd: ws });
|
|
131
|
+
assert.equal(vaultApi.notifyCalls.find(c => /unavailable/.test(c.message)), undefined);
|
|
132
|
+
assert.equal(spApi.notifyCalls.find(c => /unavailable/.test(c.message)), undefined);
|
|
133
|
+
// Memory's onDataLoad path returns null recorder → scratchpad swallows. No crash.
|
|
134
|
+
await fireSessionShutdown(spApi);
|
|
135
|
+
await fireSessionShutdown(vaultApi);
|
|
136
|
+
} finally {
|
|
137
|
+
delete process.env.OTTO_COWORKER_GLOBAL_DIR;
|
|
138
|
+
delete process.env.OTTO_SCRATCHPAD_ROOT;
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
});
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// packages/coworker-memory/src/context-injection.test.ts
|
|
2
|
+
import { describe, it } from 'node:test';
|
|
3
|
+
import assert from 'node:assert/strict';
|
|
4
|
+
import { mkdtempSync } from 'node:fs';
|
|
5
|
+
import { tmpdir } from 'node:os';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
import { AuditLog, SecretScanner } from '@otto/coworker-utils';
|
|
8
|
+
import { LayerAStore } from './layer-a-store.js';
|
|
9
|
+
import { buildLayerAContext } from './context-injection.js';
|
|
10
|
+
|
|
11
|
+
async function makeStores() {
|
|
12
|
+
const root = mkdtempSync(join(tmpdir(), 'ci-'));
|
|
13
|
+
const audit = new AuditLog({ path: join(root, 'audit.jsonl') });
|
|
14
|
+
const scanner = new SecretScanner();
|
|
15
|
+
const globalStore = new LayerAStore({ scopeDir: join(root, 'g'), scope: 'global', audit, scanner });
|
|
16
|
+
const workspaceStore = new LayerAStore({ scopeDir: join(root, 'w'), scope: 'workspace', audit, scanner });
|
|
17
|
+
return { root, globalStore, workspaceStore };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe('buildLayerAContext', () => {
|
|
21
|
+
it('returns empty when no Layer A files exist', async () => {
|
|
22
|
+
const { globalStore, workspaceStore } = await makeStores();
|
|
23
|
+
const md = await buildLayerAContext({
|
|
24
|
+
mode: 'per-project-tagged', globalStore, workspaceStore, tokenLimit: 3000,
|
|
25
|
+
});
|
|
26
|
+
assert.equal(md, '');
|
|
27
|
+
});
|
|
28
|
+
it('global mode reads global only', async () => {
|
|
29
|
+
const { globalStore, workspaceStore } = await makeStores();
|
|
30
|
+
await globalStore.append({ kind: 'profile', text: 'global profile', source: 'user', ts: '2026-06-02T00:00:00Z' });
|
|
31
|
+
await workspaceStore.append({ kind: 'profile', text: 'workspace profile', source: 'user', ts: '2026-06-02T00:00:00Z' });
|
|
32
|
+
const md = await buildLayerAContext({
|
|
33
|
+
mode: 'global', globalStore, workspaceStore, tokenLimit: 3000,
|
|
34
|
+
});
|
|
35
|
+
assert.match(md, /global profile/);
|
|
36
|
+
assert.equal(md.includes('workspace profile'), false);
|
|
37
|
+
});
|
|
38
|
+
it('per-project mode reads workspace only', async () => {
|
|
39
|
+
const { globalStore, workspaceStore } = await makeStores();
|
|
40
|
+
await globalStore.append({ kind: 'profile', text: 'global profile', source: 'user', ts: '2026-06-02T00:00:00Z' });
|
|
41
|
+
await workspaceStore.append({ kind: 'profile', text: 'workspace profile', source: 'user', ts: '2026-06-02T00:00:00Z' });
|
|
42
|
+
const md = await buildLayerAContext({
|
|
43
|
+
mode: 'per-project', globalStore, workspaceStore, tokenLimit: 3000,
|
|
44
|
+
});
|
|
45
|
+
assert.match(md, /workspace profile/);
|
|
46
|
+
assert.equal(md.includes('global profile'), false);
|
|
47
|
+
});
|
|
48
|
+
it('per-project-tagged includes both with workspace first', async () => {
|
|
49
|
+
const { globalStore, workspaceStore } = await makeStores();
|
|
50
|
+
await globalStore.append({ kind: 'rule', text: 'global rule', source: 'user', ts: '2026-06-02T00:00:00Z' });
|
|
51
|
+
await workspaceStore.append({ kind: 'rule', text: 'workspace rule', source: 'user', ts: '2026-06-02T00:00:00Z' });
|
|
52
|
+
const md = await buildLayerAContext({
|
|
53
|
+
mode: 'per-project-tagged', globalStore, workspaceStore, tokenLimit: 3000,
|
|
54
|
+
});
|
|
55
|
+
const wsIdx = md.indexOf('workspace rule');
|
|
56
|
+
const gIdx = md.indexOf('global rule');
|
|
57
|
+
assert.ok(wsIdx > 0 && gIdx > 0);
|
|
58
|
+
assert.ok(wsIdx < gIdx, 'workspace should appear before global');
|
|
59
|
+
});
|
|
60
|
+
it('truncates lower-priority files when token limit exceeded', async () => {
|
|
61
|
+
const { globalStore, workspaceStore } = await makeStores();
|
|
62
|
+
await workspaceStore.append({ kind: 'profile', text: 'p'.repeat(1000), source: 'user', ts: '2026-06-02T00:00:00Z' });
|
|
63
|
+
await workspaceStore.append({ kind: 'rule', text: 'r'.repeat(1000), source: 'user', ts: '2026-06-02T00:00:00Z' });
|
|
64
|
+
await workspaceStore.append({ kind: 'lesson', text: 'l'.repeat(1000), source: 'user', ts: '2026-06-02T00:00:00Z' });
|
|
65
|
+
const md = await buildLayerAContext({
|
|
66
|
+
mode: 'per-project', globalStore, workspaceStore, tokenLimit: 300, // ~1200 chars
|
|
67
|
+
});
|
|
68
|
+
assert.ok(md.includes('p'.repeat(50)));
|
|
69
|
+
// lessons should be dropped because lowest priority
|
|
70
|
+
assert.equal(md.includes('l'.repeat(900)), false);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// packages/coworker-memory/src/context-injection.ts
|
|
2
|
+
import type { ScopeMode } from './types.js';
|
|
3
|
+
import type { LayerAStore } from './layer-a-store.js';
|
|
4
|
+
|
|
5
|
+
export interface ContextInjectionArgs {
|
|
6
|
+
mode: ScopeMode;
|
|
7
|
+
globalStore: LayerAStore;
|
|
8
|
+
workspaceStore: LayerAStore;
|
|
9
|
+
tokenLimit: number; // approx 4 chars per token
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const CHARS_PER_TOKEN = 4;
|
|
13
|
+
|
|
14
|
+
export async function buildLayerAContext(args: ContextInjectionArgs): Promise<string> {
|
|
15
|
+
const charLimit = args.tokenLimit * CHARS_PER_TOKEN;
|
|
16
|
+
|
|
17
|
+
const readScopes: Array<'workspace' | 'global'> = args.mode === 'global'
|
|
18
|
+
? ['global']
|
|
19
|
+
: args.mode === 'per-project'
|
|
20
|
+
? ['workspace']
|
|
21
|
+
: ['workspace', 'global'];
|
|
22
|
+
|
|
23
|
+
type Section = { title: string; body: string; priority: number };
|
|
24
|
+
const sections: Section[] = [];
|
|
25
|
+
|
|
26
|
+
const PRIORITIES: Record<string, number> = { profile: 0, rules: 1, lessons: 2 };
|
|
27
|
+
|
|
28
|
+
for (const scope of readScopes) {
|
|
29
|
+
const store = scope === 'global' ? args.globalStore : args.workspaceStore;
|
|
30
|
+
const profile = await store.read('profile');
|
|
31
|
+
const rules = await store.read('rule');
|
|
32
|
+
const lessons = await store.read('lesson');
|
|
33
|
+
if (profile) sections.push({ title: `Profile (${scope})`, body: profile, priority: PRIORITIES.profile! });
|
|
34
|
+
if (rules) sections.push({ title: `Rules (${scope})`, body: rules, priority: PRIORITIES.rules! });
|
|
35
|
+
if (lessons) sections.push({ title: `Recent lessons (${scope})`, body: lessons, priority: PRIORITIES.lessons! });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (sections.length === 0) return '';
|
|
39
|
+
|
|
40
|
+
// workspace comes first because we iterated readScopes in that order; stable sort by priority.
|
|
41
|
+
sections.sort((a, b) => a.priority - b.priority);
|
|
42
|
+
|
|
43
|
+
let total = 0;
|
|
44
|
+
const include: Section[] = [];
|
|
45
|
+
for (const s of sections) {
|
|
46
|
+
const cost = s.title.length + s.body.length + 10;
|
|
47
|
+
if (total + cost > charLimit && include.length > 0) break;
|
|
48
|
+
include.push(s);
|
|
49
|
+
total += cost;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const lines = ['## Memory (Layer A)\n'];
|
|
53
|
+
for (const s of include) {
|
|
54
|
+
lines.push(`### ${s.title}\n${s.body}\n`);
|
|
55
|
+
}
|
|
56
|
+
return lines.join('\n');
|
|
57
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// packages/coworker-memory/src/errors.test.ts
|
|
2
|
+
import { describe, it } from 'node:test';
|
|
3
|
+
import assert from 'node:assert/strict';
|
|
4
|
+
import {
|
|
5
|
+
MemoryNotInitialized, BackendUnavailable, DrawerKindRejected,
|
|
6
|
+
LayerAWriteBlocked, RecallQueryMalformed, MemoryEntryMalformed,
|
|
7
|
+
} from './errors.js';
|
|
8
|
+
|
|
9
|
+
describe('memory errors', () => {
|
|
10
|
+
it('MemoryNotInitialized carries reason', () => {
|
|
11
|
+
const e = new MemoryNotInitialized('workspace.json corrupted');
|
|
12
|
+
assert.equal(e.name, 'MemoryNotInitialized');
|
|
13
|
+
assert.equal(e.reason, 'workspace.json corrupted');
|
|
14
|
+
assert.ok(e.message.includes('workspace.json corrupted'));
|
|
15
|
+
});
|
|
16
|
+
it('BackendUnavailable carries reason', () => {
|
|
17
|
+
const e = new BackendUnavailable('SQLITE_BUSY after retries');
|
|
18
|
+
assert.equal(e.name, 'BackendUnavailable');
|
|
19
|
+
assert.equal(e.reason, 'SQLITE_BUSY after retries');
|
|
20
|
+
});
|
|
21
|
+
it('DrawerKindRejected carries kind', () => {
|
|
22
|
+
const e = new DrawerKindRejected('mystery');
|
|
23
|
+
assert.equal(e.name, 'DrawerKindRejected');
|
|
24
|
+
assert.equal(e.kind, 'mystery');
|
|
25
|
+
assert.ok(e.message.includes('mystery'));
|
|
26
|
+
});
|
|
27
|
+
it('LayerAWriteBlocked carries secret_kind', () => {
|
|
28
|
+
const e = new LayerAWriteBlocked('anthropic_api_key');
|
|
29
|
+
assert.equal(e.name, 'LayerAWriteBlocked');
|
|
30
|
+
assert.equal(e.secretKind, 'anthropic_api_key');
|
|
31
|
+
assert.ok(e.message.includes('anthropic_api_key'));
|
|
32
|
+
assert.ok(e.message.includes('/connect'));
|
|
33
|
+
});
|
|
34
|
+
it('RecallQueryMalformed carries reason', () => {
|
|
35
|
+
const e = new RecallQueryMalformed('empty query');
|
|
36
|
+
assert.equal(e.name, 'RecallQueryMalformed');
|
|
37
|
+
assert.equal(e.reason, 'empty query');
|
|
38
|
+
});
|
|
39
|
+
it('MemoryEntryMalformed carries path', () => {
|
|
40
|
+
const e = new MemoryEntryMalformed('/tmp/profile.md', 'bad frontmatter');
|
|
41
|
+
assert.equal(e.name, 'MemoryEntryMalformed');
|
|
42
|
+
assert.equal(e.path, '/tmp/profile.md');
|
|
43
|
+
assert.equal(e.reason, 'bad frontmatter');
|
|
44
|
+
});
|
|
45
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// packages/coworker-memory/src/errors.ts
|
|
2
|
+
export class MemoryNotInitialized extends Error {
|
|
3
|
+
constructor(public readonly reason: string) {
|
|
4
|
+
super(`Memory not initialized: ${reason}. /memory status to inspect.`);
|
|
5
|
+
this.name = 'MemoryNotInitialized';
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export class BackendUnavailable extends Error {
|
|
10
|
+
constructor(public readonly reason: string) {
|
|
11
|
+
super(`Memory backend unavailable: ${reason}.`);
|
|
12
|
+
this.name = 'BackendUnavailable';
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export class DrawerKindRejected extends Error {
|
|
17
|
+
constructor(public readonly kind: string) {
|
|
18
|
+
super(`Drawer kind '${kind}' is not in v1 vocabulary. Allowed: turn, paste, file_load, ticket, email, rca, note.`);
|
|
19
|
+
this.name = 'DrawerKindRejected';
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class LayerAWriteBlocked extends Error {
|
|
24
|
+
constructor(public readonly secretKind: string) {
|
|
25
|
+
super(`Refused to store: contains secret-shaped value (kind: ${secretKind}). Remove the secret and retry. Vault entries should land in /connect, not memorize.`);
|
|
26
|
+
this.name = 'LayerAWriteBlocked';
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export class RecallQueryMalformed extends Error {
|
|
31
|
+
constructor(public readonly reason: string) {
|
|
32
|
+
super(`Bad recall query: ${reason}.`);
|
|
33
|
+
this.name = 'RecallQueryMalformed';
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export class MemoryEntryMalformed extends Error {
|
|
38
|
+
constructor(public readonly path: string, public readonly reason: string) {
|
|
39
|
+
super(`Layer A file ${path} is malformed: ${reason}. Move it aside and re-create.`);
|
|
40
|
+
this.name = 'MemoryEntryMalformed';
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { describe, it } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import * as memory from './index.js';
|
|
4
|
+
|
|
5
|
+
describe('@otto/coworker-memory barrel', () => {
|
|
6
|
+
it('exports the key surface', () => {
|
|
7
|
+
assert.equal(typeof memory.LocalSqliteBackend, 'function');
|
|
8
|
+
assert.equal(typeof memory.LayerAStore, 'function');
|
|
9
|
+
assert.equal(typeof memory.MemoryRecorder, 'function');
|
|
10
|
+
assert.equal(typeof memory.resolveWorkspaceId, 'function');
|
|
11
|
+
assert.equal(typeof memory.resolveScope, 'function');
|
|
12
|
+
assert.equal(typeof memory.detectPaste, 'function');
|
|
13
|
+
assert.equal(typeof memory.formatRecall, 'function');
|
|
14
|
+
assert.equal(typeof memory.buildLayerAContext, 'function');
|
|
15
|
+
assert.equal(typeof memory.applyPersonaSeed, 'function');
|
|
16
|
+
});
|
|
17
|
+
it('exports error classes', () => {
|
|
18
|
+
assert.equal(typeof memory.LayerAWriteBlocked, 'function');
|
|
19
|
+
assert.equal(typeof memory.RecallQueryMalformed, 'function');
|
|
20
|
+
});
|
|
21
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export * from './types.js';
|
|
2
|
+
export * from './errors.js';
|
|
3
|
+
export * from './memory-backend.js';
|
|
4
|
+
export * from './workspace-id.js';
|
|
5
|
+
export * from './scope-resolver.js';
|
|
6
|
+
export * from './paste-detector.js';
|
|
7
|
+
export * from './layer-a-store.js';
|
|
8
|
+
export * from './local-sqlite-backend.js';
|
|
9
|
+
export * from './memory-recorder.js';
|
|
10
|
+
export * from './recall-formatter.js';
|
|
11
|
+
export * from './context-injection.js';
|
|
12
|
+
export * from './persona-seed.js';
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// packages/coworker-memory/src/layer-a-store.test.ts
|
|
2
|
+
import { describe, it } from 'node:test';
|
|
3
|
+
import assert from 'node:assert/strict';
|
|
4
|
+
import { mkdtempSync, readFileSync, existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
5
|
+
import { tmpdir } from 'node:os';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
import { AuditLog, SecretScanner } from '@otto/coworker-utils';
|
|
8
|
+
import { LayerAStore } from './layer-a-store.js';
|
|
9
|
+
import { LayerAWriteBlocked, MemoryEntryMalformed } from './errors.js';
|
|
10
|
+
|
|
11
|
+
function ctx() {
|
|
12
|
+
const root = mkdtempSync(join(tmpdir(), 'la-'));
|
|
13
|
+
return {
|
|
14
|
+
root,
|
|
15
|
+
audit: new AuditLog({ path: join(root, 'audit.jsonl') }),
|
|
16
|
+
scanner: new SecretScanner(),
|
|
17
|
+
dir: join(root, 'memory'),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe('LayerAStore', () => {
|
|
22
|
+
it('append lesson creates lessons.md with frontmatter and bullet', async () => {
|
|
23
|
+
const c = ctx();
|
|
24
|
+
const store = new LayerAStore({ scopeDir: c.dir, scope: 'workspace', audit: c.audit, scanner: c.scanner });
|
|
25
|
+
await store.append({ kind: 'lesson', text: 'MTTR target is 30 minutes for P1', source: 'user', ts: '2026-06-02T10:00:00Z' });
|
|
26
|
+
const md = readFileSync(join(c.dir, 'lessons.md'), 'utf8');
|
|
27
|
+
assert.match(md, /^---\nschema_version: 1\n/);
|
|
28
|
+
assert.match(md, /- \(2026-06-02T10:00:00Z\) MTTR target is 30 minutes for P1/);
|
|
29
|
+
});
|
|
30
|
+
it('append profile uses timestamped section', async () => {
|
|
31
|
+
const c = ctx();
|
|
32
|
+
const store = new LayerAStore({ scopeDir: c.dir, scope: 'workspace', audit: c.audit, scanner: c.scanner });
|
|
33
|
+
await store.append({ kind: 'profile', text: 'Prefers polars over pandas.', source: 'user', ts: '2026-06-02T10:00:00Z' });
|
|
34
|
+
const md = readFileSync(join(c.dir, 'profile.md'), 'utf8');
|
|
35
|
+
assert.match(md, /## 2026-06-02T10:00:00Z\nPrefers polars over pandas\./);
|
|
36
|
+
});
|
|
37
|
+
it('throws LayerAWriteBlocked when text contains a secret pattern', async () => {
|
|
38
|
+
const c = ctx();
|
|
39
|
+
const store = new LayerAStore({ scopeDir: c.dir, scope: 'workspace', audit: c.audit, scanner: c.scanner });
|
|
40
|
+
await assert.rejects(
|
|
41
|
+
() => store.append({ kind: 'rule', text: 'use AKIAABCDEFGHIJKLMNOP', source: 'user', ts: '2026-06-02T10:00:00Z' }),
|
|
42
|
+
LayerAWriteBlocked,
|
|
43
|
+
);
|
|
44
|
+
assert.equal(existsSync(join(c.dir, 'rules.md')), false);
|
|
45
|
+
});
|
|
46
|
+
it('read returns parsed content with frontmatter stripped', async () => {
|
|
47
|
+
const c = ctx();
|
|
48
|
+
const store = new LayerAStore({ scopeDir: c.dir, scope: 'workspace', audit: c.audit, scanner: c.scanner });
|
|
49
|
+
await store.append({ kind: 'rule', text: 'Always include MTTR in RCA.', source: 'user', ts: '2026-06-02T10:00:00Z' });
|
|
50
|
+
const body = await store.read('rule');
|
|
51
|
+
assert.match(body, /Always include MTTR in RCA\./);
|
|
52
|
+
assert.equal(body.startsWith('---'), false);
|
|
53
|
+
});
|
|
54
|
+
it('read returns empty string when file missing', async () => {
|
|
55
|
+
const c = ctx();
|
|
56
|
+
const store = new LayerAStore({ scopeDir: c.dir, scope: 'workspace', audit: c.audit, scanner: c.scanner });
|
|
57
|
+
assert.equal(await store.read('lesson'), '');
|
|
58
|
+
});
|
|
59
|
+
it('emits write-layer-a audit on success', async () => {
|
|
60
|
+
const c = ctx();
|
|
61
|
+
const store = new LayerAStore({ scopeDir: c.dir, scope: 'workspace', audit: c.audit, scanner: c.scanner });
|
|
62
|
+
await store.append({ kind: 'lesson', text: 'short lesson', source: 'user', ts: '2026-06-02T10:00:00Z' });
|
|
63
|
+
const rows: { action: string }[] = [];
|
|
64
|
+
for await (const r of c.audit.read({ producer: 'memory', action: 'write-layer-a' })) rows.push(r as never);
|
|
65
|
+
assert.equal(rows.length, 1);
|
|
66
|
+
});
|
|
67
|
+
it('re-appending to the same kind does not duplicate the title heading', async () => {
|
|
68
|
+
const c = ctx();
|
|
69
|
+
const store = new LayerAStore({ scopeDir: c.dir, scope: 'workspace', audit: c.audit, scanner: c.scanner });
|
|
70
|
+
await store.append({ kind: 'lesson', text: 'first lesson', source: 'user', ts: '2026-06-02T10:00:00Z' });
|
|
71
|
+
await store.append({ kind: 'lesson', text: 'second lesson', source: 'user', ts: '2026-06-02T10:05:00Z' });
|
|
72
|
+
const raw = readFileSync(join(c.dir, 'lessons.md'), 'utf8');
|
|
73
|
+
const titleCount = (raw.match(/^# Lessons$/gm) ?? []).length;
|
|
74
|
+
assert.equal(titleCount, 1, 'duplicate title heading after re-append');
|
|
75
|
+
assert.match(raw, /first lesson/);
|
|
76
|
+
assert.match(raw, /second lesson/);
|
|
77
|
+
});
|
|
78
|
+
it('read throws MemoryEntryMalformed on unterminated frontmatter', async () => {
|
|
79
|
+
const c = ctx();
|
|
80
|
+
mkdirSync(c.dir, { recursive: true, mode: 0o700 });
|
|
81
|
+
writeFileSync(join(c.dir, 'rules.md'), '---\nbroken-yaml: [\n', { mode: 0o600 });
|
|
82
|
+
const store = new LayerAStore({ scopeDir: c.dir, scope: 'workspace', audit: c.audit, scanner: c.scanner });
|
|
83
|
+
await assert.rejects(() => store.read('rule'), MemoryEntryMalformed);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// packages/coworker-memory/src/layer-a-store.ts
|
|
2
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { parse as parseYaml, stringify as stringifyYaml } from 'yaml';
|
|
5
|
+
import { AuditLog, SecretScanner } from '@otto/coworker-utils';
|
|
6
|
+
import type { LayerAEntry, LayerAKind } from './types.js';
|
|
7
|
+
import { LayerAWriteBlocked, MemoryEntryMalformed } from './errors.js';
|
|
8
|
+
|
|
9
|
+
const FILE_FOR: Record<LayerAKind, string> = {
|
|
10
|
+
profile: 'profile.md',
|
|
11
|
+
rule: 'rules.md',
|
|
12
|
+
lesson: 'lessons.md',
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const TITLE_FOR: Record<LayerAKind, string> = {
|
|
16
|
+
profile: 'Profile',
|
|
17
|
+
rule: 'Rules',
|
|
18
|
+
lesson: 'Lessons',
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export interface LayerAStoreOptions {
|
|
22
|
+
scopeDir: string; // absolute path to scope's memory dir
|
|
23
|
+
scope: 'global' | 'workspace';
|
|
24
|
+
audit: AuditLog;
|
|
25
|
+
scanner: SecretScanner;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export class LayerAStore {
|
|
29
|
+
constructor(private readonly opts: LayerAStoreOptions) {}
|
|
30
|
+
|
|
31
|
+
async append(entry: LayerAEntry): Promise<void> {
|
|
32
|
+
const hits = this.opts.scanner.scan(entry.text);
|
|
33
|
+
if (hits.length > 0) {
|
|
34
|
+
this.opts.audit.append({
|
|
35
|
+
_schema: 1, ts: new Date().toISOString(), producer: 'memory', action: 'block', severity: 'warn',
|
|
36
|
+
detail: { scope: this.opts.scope, kind: entry.kind, reason: 'secret', secret_kind: hits[0]!.kind },
|
|
37
|
+
});
|
|
38
|
+
throw new LayerAWriteBlocked(hits[0]!.kind);
|
|
39
|
+
}
|
|
40
|
+
mkdirSync(this.opts.scopeDir, { recursive: true, mode: 0o700 });
|
|
41
|
+
const path = join(this.opts.scopeDir, FILE_FOR[entry.kind]);
|
|
42
|
+
const existing = existsSync(path) ? readFileSync(path, 'utf8') : null;
|
|
43
|
+
const { body } = existing ? this.split(existing, path) : { body: '' };
|
|
44
|
+
const addition = entry.kind === 'lesson'
|
|
45
|
+
? `- (${entry.ts}) ${entry.text}\n`
|
|
46
|
+
: `## ${entry.ts}\n${entry.text}\n\n`;
|
|
47
|
+
const newBody = (body && !body.endsWith('\n') ? body + '\n' : body) + addition;
|
|
48
|
+
const newFile = this.composeFile(entry.kind, newBody, entry.ts, entry.source);
|
|
49
|
+
const tmp = `${path}.tmp`;
|
|
50
|
+
writeFileSync(tmp, newFile, { mode: 0o600 });
|
|
51
|
+
chmodSync(tmp, 0o600);
|
|
52
|
+
renameSync(tmp, path);
|
|
53
|
+
this.opts.audit.append({
|
|
54
|
+
_schema: 1, ts: new Date().toISOString(), producer: 'memory', action: 'write-layer-a',
|
|
55
|
+
detail: { scope: this.opts.scope, kind: entry.kind, source: entry.source, byte_count: Buffer.byteLength(entry.text, 'utf8') },
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async read(kind: LayerAKind): Promise<string> {
|
|
60
|
+
const path = join(this.opts.scopeDir, FILE_FOR[kind]);
|
|
61
|
+
if (!existsSync(path)) return '';
|
|
62
|
+
const raw = readFileSync(path, 'utf8');
|
|
63
|
+
const { body } = this.split(raw, path);
|
|
64
|
+
return body.trim();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
private split(raw: string, path: string): { frontmatter: Record<string, unknown>; body: string } {
|
|
68
|
+
if (!raw.startsWith('---')) {
|
|
69
|
+
return { frontmatter: {}, body: raw };
|
|
70
|
+
}
|
|
71
|
+
const end = raw.indexOf('\n---\n', 4);
|
|
72
|
+
if (end < 0) throw new MemoryEntryMalformed(path, 'unterminated frontmatter');
|
|
73
|
+
try {
|
|
74
|
+
const fm = parseYaml(raw.slice(4, end)) as Record<string, unknown>;
|
|
75
|
+
const body = raw.slice(end + 5);
|
|
76
|
+
return { frontmatter: fm ?? {}, body };
|
|
77
|
+
} catch (err) {
|
|
78
|
+
throw new MemoryEntryMalformed(path, `frontmatter parse: ${(err as Error).message}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
private composeFile(kind: LayerAKind, body: string, ts: string, source: LayerAEntry['source']): string {
|
|
83
|
+
const fm = { schema_version: 1, last_modified_at: ts, source };
|
|
84
|
+
const fmStr = stringifyYaml(fm).trimEnd();
|
|
85
|
+
const header = `---\n${fmStr}\n---\n\n# ${TITLE_FOR[kind]}\n\n`;
|
|
86
|
+
return header + body.replace(new RegExp(`^\\n?# ${TITLE_FOR[kind]}\\n\\n?`), '');
|
|
87
|
+
}
|
|
88
|
+
}
|