@mnstry/atelier 0.2.0-alpha.5 → 0.2.0-alpha.8
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/CHANGELOG.md +191 -0
- package/README.md +86 -25
- package/SECURITY.md +6 -1
- package/contracts/atelier-adoption-policy.v1.schema.json +49 -0
- package/contracts/atelier-coauthor.v1.schema.json +163 -0
- package/contracts/atelier-decision-request.v1.schema.json +476 -0
- package/contracts/atelier-decision-result.v1.schema.json +500 -0
- package/contracts/atelier-guide.v1.schema.json +227 -0
- package/contracts/atelier-intake.v1.schema.json +151 -0
- package/contracts/atelier-migration.v2.schema.json +61 -0
- package/contracts/atelier-obsidian-acceptance-receipt.v1.schema.json +211 -0
- package/contracts/atelier-obsidian-apply-policy.v1.schema.json +298 -0
- package/contracts/atelier-obsidian-corpus-profile.v1.schema.json +98 -0
- package/contracts/atelier-obsidian-edit-operation.v1.schema.json +138 -0
- package/contracts/atelier-obsidian-ext-settings.v1.schema.json +287 -0
- package/contracts/atelier-obsidian-generation-manifest.v1.schema.json +353 -0
- package/contracts/atelier-obsidian-proposal-receipt.v1.schema.json +110 -0
- package/contracts/atelier-obsidian-publication-journal.v1.schema.json +167 -0
- package/contracts/atelier-obsidian-scope.v1.schema.json +264 -0
- package/contracts/atelier-obsidian-service-state.v1.schema.json +149 -0
- package/contracts/atelier-obsidian-source-snapshot.v1.schema.json +179 -0
- package/contracts/atelier-pane-presentation.v1.schema.json +76 -0
- package/contracts/atelier-presentation.v1.schema.json +1293 -0
- package/contracts/atelier-repository-observation.v1.schema.json +163 -0
- package/contracts/atelier-review.v1.schema.json +1601 -0
- package/contracts/atelier-skill-steward.v1.schema.json +692 -0
- package/contracts/atelier-upgrade-plan.v2.schema.json +398 -0
- package/contracts/atelier-upgrade-receipt.v1.schema.json +99 -0
- package/contracts/atelier-vault-publication.v1.schema.json +58 -0
- package/docs/assurance-controls.md +12 -2
- package/docs/atelier-runtime.md +13 -2
- package/docs/atelier-sync.md +172 -0
- package/docs/blocks/claims.md +28 -18
- package/docs/blocks/will-not-do.md +12 -3
- package/docs/coauthor-session.md +57 -0
- package/docs/connected-composition.md +75 -0
- package/docs/decisions.md +167 -0
- package/docs/exact-upgrades.md +159 -0
- package/docs/extension-pack-lifecycle.md +33 -0
- package/docs/guided-upgrades.md +99 -0
- package/docs/install.md +65 -9
- package/docs/intake-and-guides.md +65 -0
- package/docs/integration-and-review-plan.md +409 -0
- package/docs/integration-contract-decisions.md +22 -0
- package/docs/local-review.md +84 -0
- package/docs/local-services.md +227 -0
- package/docs/local-state.md +107 -0
- package/docs/obsidian-contract.md +687 -0
- package/docs/obsidian.md +387 -0
- package/docs/portable-presentation.md +328 -0
- package/docs/presentation-consumer-boundaries.md +75 -0
- package/docs/presentation-review-disposition.md +47 -0
- package/docs/project-command-inventory.md +38 -0
- package/docs/project-options.md +53 -0
- package/docs/release-engineering.md +99 -2
- package/docs/review-portability.md +55 -0
- package/docs/skill-steward.md +156 -0
- package/docs/upgrade.md +70 -2
- package/docs/vault-service.md +292 -0
- package/fixtures/atelier-coauthor/invalid/authority.v1.json +5 -0
- package/fixtures/atelier-coauthor/valid/config.v1.json +12 -0
- package/fixtures/atelier-guide/invalid/authority.v1.json +11 -0
- package/fixtures/atelier-guide/valid/capability.v1.json +10 -0
- package/fixtures/atelier-guide/valid/consent.v1.json +11 -0
- package/fixtures/atelier-guide/valid/engagement.v1.json +7 -0
- package/fixtures/atelier-guide/valid/offer.v1.json +10 -0
- package/fixtures/atelier-intake/invalid/authority.v1.json +9 -0
- package/fixtures/atelier-intake/valid/attempt.v1.json +8 -0
- package/fixtures/atelier-intake/valid/completion.v1.json +9 -0
- package/fixtures/atelier-intake/valid/source.v1.json +7 -0
- package/fixtures/atelier-repository-observation/invalid/complete-with-blocker.v1.json +18 -0
- package/fixtures/atelier-repository-observation/valid/complete-local.v1.json +48 -0
- package/fixtures/atelier-review/invalid/authenticated-identity.v1.json +22 -0
- package/fixtures/atelier-review/valid/bound.v1.json +229 -0
- package/fixtures/atelier-review/valid/bundle.v1.json +267 -0
- package/fixtures/atelier-review/valid/contribution.v1.json +22 -0
- package/fixtures/atelier-review/valid/lifecycle.v1.json +14 -0
- package/fixtures/atelier-skill-steward/audit/invalid/source-mutation.v1.json +26 -0
- package/fixtures/atelier-skill-steward/audit/valid/clean.v1.json +26 -0
- package/fixtures/atelier-skill-steward/candidates/invalid/workflow-key.v1.json +37 -0
- package/fixtures/atelier-skill-steward/candidates/valid/create.v1.json +37 -0
- package/fixtures/atelier-skill-steward/lock/invalid/bad-digest.v1.json +18 -0
- package/fixtures/atelier-skill-steward/lock/valid/installed.v1.json +23 -0
- package/fixtures/atelier-skill-steward/plan/invalid/delete-action.v1.json +33 -0
- package/fixtures/atelier-skill-steward/plan/valid/add.v1.json +33 -0
- package/fixtures/atelier-upgrade-transaction/adoption-policy/invalid/example.json +12 -0
- package/fixtures/atelier-upgrade-transaction/adoption-policy/valid/example.json +11 -0
- package/fixtures/atelier-upgrade-transaction/migration/invalid/example.json +20 -0
- package/fixtures/atelier-upgrade-transaction/migration/valid/example.json +19 -0
- package/fixtures/atelier-upgrade-transaction/upgrade-plan/invalid/example.json +58 -0
- package/fixtures/atelier-upgrade-transaction/upgrade-plan/valid/example.json +57 -0
- package/fixtures/atelier-upgrade-transaction/upgrade-receipt/invalid/example.json +15 -0
- package/fixtures/atelier-upgrade-transaction/upgrade-receipt/valid/example.json +14 -0
- package/fixtures/atelier-vault-publication/invalid/publication.json +5 -0
- package/fixtures/atelier-vault-publication/valid/publication.json +11 -0
- package/fixtures/decisions/request/invalid/unknown-authority.v1.json +61 -0
- package/fixtures/decisions/request/valid/triage.v1.json +60 -0
- package/fixtures/decisions/result/invalid/execution-authority.v1.json +50 -0
- package/fixtures/decisions/result/valid/abstained.v1.json +23 -0
- package/fixtures/decisions/result/valid/assessed.v1.json +50 -0
- package/fixtures/obsidian/acceptance/ap05-extra-notes.json +14 -0
- package/fixtures/obsidian/acceptance/receipts/G07.valid.v1.json +63 -0
- package/fixtures/obsidian/acceptance/receipts/G13.valid.v1.json +69 -0
- package/fixtures/obsidian/acceptance/receipts/G14.valid.v1.json +62 -0
- package/fixtures/obsidian/acceptance/receipts/G15.valid.v1.json +56 -0
- package/fixtures/obsidian/acceptance/receipts/G16.valid.v1.json +73 -0
- package/fixtures/obsidian/acceptance/receipts/G17.valid.v1.json +75 -0
- package/fixtures/obsidian/acceptance/receipts/G18.valid.v1.json +64 -0
- package/fixtures/obsidian/acceptance/service-sentinels.json +10 -0
- package/fixtures/obsidian/contracts/acceptance-receipt/invalid/duplicate-evidence-name.v1.json +39 -0
- package/fixtures/obsidian/contracts/acceptance-receipt/invalid/evidence-by-absolute-path.v1.json +34 -0
- package/fixtures/obsidian/contracts/acceptance-receipt/invalid/missing-candidate-tree.v1.json +33 -0
- package/fixtures/obsidian/contracts/acceptance-receipt/invalid/no-evidence.v1.json +28 -0
- package/fixtures/obsidian/contracts/acceptance-receipt/invalid/unknown-outcome.v1.json +34 -0
- package/fixtures/obsidian/contracts/acceptance-receipt/invalid/unknown-top-level-field.v1.json +35 -0
- package/fixtures/obsidian/contracts/acceptance-receipt/valid/node-gate.v1.json +34 -0
- package/fixtures/obsidian/contracts/apply-policy/invalid/automatic-without-edit-class.v1.json +27 -0
- package/fixtures/obsidian/contracts/apply-policy/invalid/conflict-overwrite.v1.json +29 -0
- package/fixtures/obsidian/contracts/apply-policy/invalid/missing-policy-digest.v1.json +28 -0
- package/fixtures/obsidian/contracts/apply-policy/invalid/unbounded-batch.v1.json +29 -0
- package/fixtures/obsidian/contracts/apply-policy/invalid/unimplemented-edit-class.v1.json +30 -0
- package/fixtures/obsidian/contracts/apply-policy/invalid/unknown-mode.v1.json +29 -0
- package/fixtures/obsidian/contracts/apply-policy/invalid/unknown-selector-operator.v1.json +22 -0
- package/fixtures/obsidian/contracts/apply-policy/invalid/unknown-top-level-field.v1.json +30 -0
- package/fixtures/obsidian/contracts/apply-policy/valid/automatic-scoped.v1.json +29 -0
- package/fixtures/obsidian/contracts/apply-policy/valid/manual-default.v1.json +20 -0
- package/fixtures/obsidian/contracts/corpus-profile/invalid/absolute-path-in-ext.v1.json +30 -0
- package/fixtures/obsidian/contracts/corpus-profile/invalid/absolute-repository-root.v1.json +27 -0
- package/fixtures/obsidian/contracts/corpus-profile/invalid/duplicate-repository-id.v1.json +27 -0
- package/fixtures/obsidian/contracts/corpus-profile/invalid/overlapping-managed-roots.v1.json +27 -0
- package/fixtures/obsidian/contracts/corpus-profile/invalid/unknown-enrollment.v1.json +27 -0
- package/fixtures/obsidian/contracts/corpus-profile/invalid/unknown-top-level-field.v1.json +28 -0
- package/fixtures/obsidian/contracts/corpus-profile/valid/three-repositories.v1.json +27 -0
- package/fixtures/obsidian/contracts/edit-operation/invalid/absolute-recovery-ref.v1.json +21 -0
- package/fixtures/obsidian/contracts/edit-operation/invalid/inline-observed-bytes.v1.json +22 -0
- package/fixtures/obsidian/contracts/edit-operation/invalid/missing-base-digest.v1.json +20 -0
- package/fixtures/obsidian/contracts/edit-operation/invalid/short-idempotency-key.v1.json +21 -0
- package/fixtures/obsidian/contracts/edit-operation/invalid/unknown-kind.v1.json +21 -0
- package/fixtures/obsidian/contracts/edit-operation/valid/pending-body-replacement.v1.json +21 -0
- package/fixtures/obsidian/contracts/ext-settings/invalid/absolute-path-prefix.v1.json +28 -0
- package/fixtures/obsidian/contracts/ext-settings/invalid/apply-policy-in-portable-settings.v1.json +30 -0
- package/fixtures/obsidian/contracts/ext-settings/invalid/duplicate-scope-id.v1.json +27 -0
- package/fixtures/obsidian/contracts/ext-settings/invalid/machine-local-vault-path.v1.json +28 -0
- package/fixtures/obsidian/contracts/ext-settings/invalid/service-port-in-portable-settings.v1.json +28 -0
- package/fixtures/obsidian/contracts/ext-settings/invalid/unknown-default-scope.v1.json +27 -0
- package/fixtures/obsidian/contracts/ext-settings/valid/disabled.v1.json +5 -0
- package/fixtures/obsidian/contracts/ext-settings/valid/enabled-with-scopes.v1.json +27 -0
- package/fixtures/obsidian/contracts/generation-manifest/invalid/absolute-note-path.v1.json +114 -0
- package/fixtures/obsidian/contracts/generation-manifest/invalid/complete-with-unwritten-notes.v1.json +114 -0
- package/fixtures/obsidian/contracts/generation-manifest/invalid/derived-link-claimed-declared.v1.json +114 -0
- package/fixtures/obsidian/contracts/generation-manifest/invalid/duplicate-note-identity.v1.json +114 -0
- package/fixtures/obsidian/contracts/generation-manifest/invalid/in-scope-endpoint-missing.v1.json +114 -0
- package/fixtures/obsidian/contracts/generation-manifest/invalid/title-only-note-path.v1.json +114 -0
- package/fixtures/obsidian/contracts/generation-manifest/invalid/unknown-note-field.v1.json +115 -0
- package/fixtures/obsidian/contracts/generation-manifest/invalid/unknown-relation-type.v1.json +114 -0
- package/fixtures/obsidian/contracts/generation-manifest/invalid/withheld-endpoint.v1.json +114 -0
- package/fixtures/obsidian/contracts/generation-manifest/valid/two-notes.v1.json +114 -0
- package/fixtures/obsidian/contracts/oracles/scope-cases.json +412 -0
- package/fixtures/obsidian/contracts/proposal-receipt/invalid/accepted-without-proposal-id.v1.json +12 -0
- package/fixtures/obsidian/contracts/proposal-receipt/invalid/missing-adapter-operation-id.v1.json +11 -0
- package/fixtures/obsidian/contracts/proposal-receipt/invalid/unknown-backpressure-outcome.v1.json +12 -0
- package/fixtures/obsidian/contracts/proposal-receipt/invalid/unknown-top-level-field.v1.json +13 -0
- package/fixtures/obsidian/contracts/proposal-receipt/valid/accepted-new.v1.json +12 -0
- package/fixtures/obsidian/contracts/proposal-receipt/valid/deferred.v1.json +12 -0
- package/fixtures/obsidian/contracts/publication-journal/invalid/absolute-recovery-ref.v1.json +47 -0
- package/fixtures/obsidian/contracts/publication-journal/invalid/duplicate-sequence.v1.json +47 -0
- package/fixtures/obsidian/contracts/publication-journal/invalid/empty-protocol-id.v1.json +47 -0
- package/fixtures/obsidian/contracts/publication-journal/invalid/missing-protocol-id.v1.json +46 -0
- package/fixtures/obsidian/contracts/publication-journal/invalid/unknown-entry-field.v1.json +48 -0
- package/fixtures/obsidian/contracts/publication-journal/invalid/unknown-state.v1.json +47 -0
- package/fixtures/obsidian/contracts/publication-journal/valid/committed.v1.json +47 -0
- package/fixtures/obsidian/contracts/publication-journal/valid/updating-after-restart.v1.json +22 -0
- package/fixtures/obsidian/contracts/scope/invalid/absolute-path-prefix.v1.json +15 -0
- package/fixtures/obsidian/contracts/scope/invalid/duplicate-ids.v1.json +18 -0
- package/fixtures/obsidian/contracts/scope/invalid/expansion-without-node-budget.v1.json +40 -0
- package/fixtures/obsidian/contracts/scope/invalid/full-mode-with-subset.v1.json +8 -0
- package/fixtures/obsidian/contracts/scope/invalid/unknown-mode.v1.json +41 -0
- package/fixtures/obsidian/contracts/scope/invalid/unknown-selector-key.v1.json +15 -0
- package/fixtures/obsidian/contracts/scope/invalid/unknown-selector-operator.v1.json +14 -0
- package/fixtures/obsidian/contracts/scope/invalid/unknown-top-level-field.v1.json +42 -0
- package/fixtures/obsidian/contracts/scope/valid/explicit-empty.v1.json +8 -0
- package/fixtures/obsidian/contracts/scope/valid/focus.v1.json +11 -0
- package/fixtures/obsidian/contracts/scope/valid/full.v1.json +8 -0
- package/fixtures/obsidian/contracts/scope/valid/scoped-set-expression.v1.json +41 -0
- package/fixtures/obsidian/contracts/service-state/invalid/hostname-instead-of-literal-loopback.v1.json +23 -0
- package/fixtures/obsidian/contracts/service-state/invalid/missing-consent.v1.json +18 -0
- package/fixtures/obsidian/contracts/service-state/invalid/privileged-port.v1.json +23 -0
- package/fixtures/obsidian/contracts/service-state/invalid/unknown-top-level-field.v1.json +24 -0
- package/fixtures/obsidian/contracts/service-state/invalid/wildcard-bind.v1.json +23 -0
- package/fixtures/obsidian/contracts/service-state/valid/healthy.v1.json +23 -0
- package/fixtures/obsidian/contracts/source-snapshot/invalid/absolute-file-path.v1.json +48 -0
- package/fixtures/obsidian/contracts/source-snapshot/invalid/duplicate-repository-id.v1.json +48 -0
- package/fixtures/obsidian/contracts/source-snapshot/invalid/mixed-read.v1.json +48 -0
- package/fixtures/obsidian/contracts/source-snapshot/invalid/parent-traversal-path.v1.json +48 -0
- package/fixtures/obsidian/contracts/source-snapshot/invalid/unknown-file-field.v1.json +49 -0
- package/fixtures/obsidian/contracts/source-snapshot/valid/two-repositories.v1.json +48 -0
- package/fixtures/obsidian/edits/workspace.json +65 -0
- package/fixtures/obsidian/materialization/expected-full.json +99 -0
- package/fixtures/obsidian/materialization/expected-scoped.json +39 -0
- package/fixtures/obsidian/materialization/workspace.json +54 -0
- package/fixtures/obsidian/proposals/expected-shapes.json +11 -0
- package/fixtures/ui/presentation/pane.v1.json +7 -0
- package/fixtures/ui/presentation/reference.v1.json +293 -0
- package/package.json +61 -6
- package/skills/claude/atelier-guided-coauthor/SKILL.md +49 -0
- package/skills/claude/atelier-guided-upgrade/SKILL.md +97 -0
- package/skills/claude/atelier-open/SKILL.md +5 -0
- package/skills/claude/atelier-skill-steward/SKILL.md +62 -0
- package/skills/claude/mnstry-readiness/SKILL.md +5 -0
- package/skills/codex/atelier-guided-coauthor/SKILL.md +49 -0
- package/skills/codex/atelier-guided-upgrade/SKILL.md +97 -0
- package/skills/codex/atelier-open/SKILL.md +5 -0
- package/skills/codex/atelier-skill-steward/SKILL.md +62 -0
- package/skills/codex/mnstry-readiness/SKILL.md +5 -0
- package/src/access/preview-enforcer.mjs +84 -0
- package/src/boundary/content-rules.mjs +9 -4
- package/src/boundary/policy.mjs +74 -42
- package/src/cli/project-options.mjs +58 -0
- package/src/cli/run.mjs +61 -3
- package/src/coauthor/session.mjs +161 -0
- package/src/coauthor/store.mjs +173 -0
- package/src/collaboration/event-ledger.mjs +16 -33
- package/src/collaboration/inspection-bundle.mjs +223 -0
- package/src/collaboration/review-contracts.mjs +25 -0
- package/src/collaboration/review-store.mjs +305 -0
- package/src/commands/coauthor.mjs +26 -0
- package/src/commands/config.mjs +6 -1
- package/src/commands/extension-pack.mjs +15 -8
- package/src/commands/init.mjs +44 -5
- package/src/commands/obsidian.mjs +368 -0
- package/src/commands/review.mjs +147 -0
- package/src/commands/setup.mjs +51 -14
- package/src/commands/skills.mjs +199 -0
- package/src/commands/sync.mjs +107 -0
- package/src/composition/registry.mjs +27 -0
- package/src/composition/wire.mjs +122 -0
- package/src/contracts/corpus.mjs +35 -0
- package/src/decisions/contracts.d.mts +96 -0
- package/src/decisions/contracts.mjs +321 -0
- package/src/disclosure/content-scan.mjs +13 -0
- package/src/export/atelier-export-contract.mjs +16 -1
- package/src/extension-packs/lifecycle.mjs +154 -0
- package/src/graph/graph.mjs +64 -2
- package/src/graph/knowledge-graph.mjs +548 -24
- package/src/guides/contracts.mjs +54 -0
- package/src/index.mjs +29 -0
- package/src/intake/store.mjs +119 -0
- package/src/preview/channel.mjs +89 -0
- package/src/project/config.mjs +50 -49
- package/src/project/durable-state.mjs +161 -0
- package/src/project/file-class.mjs +166 -0
- package/src/project/private-state.mjs +11 -1
- package/src/projection/obsidian/contracts.mjs +571 -0
- package/src/projection/obsidian/edits/align.mjs +169 -0
- package/src/projection/obsidian/edits/apply.mjs +889 -0
- package/src/projection/obsidian/edits/arbitrate.mjs +414 -0
- package/src/projection/obsidian/edits/contribution.mjs +108 -0
- package/src/projection/obsidian/edits/index.mjs +19 -0
- package/src/projection/obsidian/edits/object-identity.mjs +61 -0
- package/src/projection/obsidian/edits/object-store.mjs +517 -0
- package/src/projection/obsidian/edits/observe.mjs +123 -0
- package/src/projection/obsidian/edits/policy.mjs +123 -0
- package/src/projection/obsidian/edits/regions.mjs +648 -0
- package/src/projection/obsidian/materialize/byte-lens.mjs +98 -0
- package/src/projection/obsidian/materialize/index.mjs +7 -0
- package/src/projection/obsidian/materialize/path-registry.mjs +105 -0
- package/src/projection/obsidian/materialize/prepare-view.mjs +746 -0
- package/src/projection/obsidian/materialize/settings.mjs +57 -0
- package/src/projection/obsidian/materialize/stage.mjs +27 -0
- package/src/projection/obsidian/proposals/adapter.mjs +572 -0
- package/src/projection/obsidian/proposals/backpressure.mjs +113 -0
- package/src/projection/obsidian/proposals/contribution.mjs +55 -0
- package/src/projection/obsidian/proposals/index.mjs +23 -0
- package/src/projection/obsidian/proposals/observation.mjs +216 -0
- package/src/projection/obsidian/proposals/queue.mjs +277 -0
- package/src/projection/obsidian/proposals/router.mjs +168 -0
- package/src/projection/obsidian/publication/bridge-script.mjs +266 -0
- package/src/projection/obsidian/publication/exchange.mjs +117 -0
- package/src/projection/obsidian/publication/index.mjs +5 -0
- package/src/projection/obsidian/publication/publisher.mjs +588 -0
- package/src/projection/obsidian/publication/test-seam.mjs +16 -0
- package/src/projection/obsidian/publication/transport.mjs +139 -0
- package/src/projection/obsidian/recovery/index.mjs +6 -0
- package/src/projection/obsidian/recovery/journal.mjs +134 -0
- package/src/projection/obsidian/recovery/late-writer.mjs +46 -0
- package/src/projection/obsidian/recovery/restart.mjs +316 -0
- package/src/projection/obsidian/recovery/store.mjs +234 -0
- package/src/projection/obsidian/selection-ui/conflict-view.mjs +91 -0
- package/src/projection/obsidian/selection-ui/contribution.mjs +141 -0
- package/src/projection/obsidian/selection-ui/focus.mjs +82 -0
- package/src/projection/obsidian/selection-ui/index.mjs +12 -0
- package/src/projection/obsidian/selection-ui/policy-setup.mjs +107 -0
- package/src/projection/obsidian/selection-ui/receipt.mjs +173 -0
- package/src/projection/obsidian/selection-ui/selection-state.mjs +122 -0
- package/src/projection/obsidian/selection-ui/selection.mjs +109 -0
- package/src/projection/project.mjs +28 -19
- package/src/readiness-protocols/evidence.mjs +282 -0
- package/src/readiness-protocols/runtime.mjs +4 -3
- package/src/readiness-protocols/source-read.mjs +39 -0
- package/src/runtime/git-adapter.mjs +189 -0
- package/src/runtime/local-state.mjs +439 -0
- package/src/runtime/obsidian/app-capability.mjs +110 -0
- package/src/runtime/obsidian/app-production-seams.mjs +96 -0
- package/src/runtime/obsidian/contributions/proposal-adapter.mjs +4 -0
- package/src/runtime/obsidian/contributions/selection-ui.mjs +4 -0
- package/src/runtime/obsidian/contributions/source-apply.mjs +4 -0
- package/src/runtime/obsidian/contributions.mjs +27 -0
- package/src/runtime/obsidian/documents.mjs +37 -0
- package/src/runtime/obsidian/enablement.mjs +53 -0
- package/src/runtime/obsidian/engine.mjs +532 -0
- package/src/runtime/obsidian/errors.mjs +14 -0
- package/src/runtime/obsidian/extension-points.mjs +103 -0
- package/src/runtime/obsidian/index.mjs +34 -0
- package/src/runtime/obsidian/lifecycle.mjs +258 -0
- package/src/runtime/obsidian/machine-settings.mjs +260 -0
- package/src/runtime/obsidian/observation.mjs +158 -0
- package/src/runtime/obsidian/opening.mjs +243 -0
- package/src/runtime/obsidian/pending-edits.mjs +141 -0
- package/src/runtime/obsidian/pipeline.mjs +122 -0
- package/src/runtime/obsidian/private-lock.mjs +156 -0
- package/src/runtime/obsidian/process-identity.mjs +58 -0
- package/src/runtime/obsidian/service-client.mjs +66 -0
- package/src/runtime/obsidian/service-main.mjs +89 -0
- package/src/runtime/obsidian/service-record.mjs +169 -0
- package/src/runtime/obsidian/service-server.mjs +121 -0
- package/src/runtime/obsidian/service.mjs +216 -0
- package/src/runtime/obsidian/startup-adapters.mjs +87 -0
- package/src/runtime/obsidian/state-store.mjs +180 -0
- package/src/runtime/obsidian/sync-notice.mjs +33 -0
- package/src/runtime/obsidian/tick-loop.mjs +84 -0
- package/src/runtime/obsidian/watchers.mjs +36 -0
- package/src/runtime/repository-observation.mjs +491 -0
- package/src/runtime/supervisor.mjs +803 -0
- package/src/server/local-sidecar.mjs +44 -0
- package/src/server/server.mjs +1 -0
- package/src/skills/steward.mjs +780 -0
- package/src/ui/presentation/browser.mjs +278 -0
- package/src/ui/presentation/contract.mjs +116 -0
- package/src/ui/presentation/index.mjs +7 -0
- package/src/ui/presentation/native.mjs +153 -0
- package/src/ui/presentation/proof.mjs +57 -0
- package/src/ui/presentation/schema-check.mjs +30 -0
- package/src/ui/presentation/schema.generated.mjs +1371 -0
- package/src/ui/presentation/state.mjs +43 -0
- package/src/ui/presentation/styles.mjs +67 -0
- package/src/ui/presentation/tokens.mjs +74 -0
- package/src/ui/presentation/web.mjs +85 -0
- package/src/ui/review-page.mjs +32 -0
- package/src/upgrade/explanation.mjs +33 -0
- package/src/upgrade/provenance.mjs +338 -0
- package/src/upgrade/transaction-files.mjs +103 -0
- package/src/upgrade/transaction.mjs +475 -0
- package/src/upgrade/upgrade.mjs +63 -28
- package/src/vault/hosts.mjs +21 -0
- package/src/vault/identity.mjs +19 -0
- package/src/vault/index.mjs +7 -0
- package/src/vault/interface.mjs +11 -0
- package/src/vault/metadata.mjs +31 -0
- package/src/vault/privacy.mjs +148 -0
- package/src/vault/probe.mjs +89 -0
- package/src/vault/service.mjs +131 -0
- package/src/vault/source.mjs +45 -0
- package/src/vault/storage.mjs +23 -0
- package/templates/distribution-workspace/.claude/launch.json +6 -2
- package/templates/distribution-workspace/gitignore +1 -0
- package/templates/distribution-workspace/project/README.md +2 -2
- package/templates/external-project-workspace/README.md +45 -0
- package/templates/external-project-workspace/answers.example.json +4 -0
- package/templates/external-project-workspace/atelier.project.json +40 -0
- package/templates/external-project-workspace/boundary-policy.v1.json +60 -0
- package/templates/external-project-workspace/gitignore +8 -0
- package/templates/external-project-workspace/pack-lifecycle.json +14 -0
- package/templates/external-project-workspace/packs/protocols/contract-gate.v1.json +109 -0
- package/templates/external-project-workspace/packs/sample-pack.v1.json +48 -0
- package/templates/external-project-workspace/repo-access.v1.json +9 -0
- package/templates/external-project-workspace/source/README.md +11 -0
- package/templates/launch.json +6 -2
- package/templates/private-domain-workspace/.claude/launch.json +6 -2
- package/templates/private-domain-workspace/domain/README.md +2 -2
- package/templates/private-domain-workspace/gitignore +1 -0
- package/templates/shared-project-workspace/.claude/launch.json +6 -2
- package/templates/shared-project-workspace/gitignore +1 -0
- package/templates/shared-project-workspace/project/README.md +2 -2
|
@@ -0,0 +1,780 @@
|
|
|
1
|
+
import crypto from 'node:crypto'
|
|
2
|
+
import fs from 'node:fs'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { execFileSync } from 'node:child_process'
|
|
5
|
+
|
|
6
|
+
import { createCollaborationEventLedger } from '../collaboration/event-ledger.mjs'
|
|
7
|
+
import { validateJsonSchema } from '../export/atelier-export-contract.mjs'
|
|
8
|
+
import { atomicReplacePrivateText, ensureContainedPrivateDirectory, openRegularFileNoFollow } from '../project/private-state.mjs'
|
|
9
|
+
import { packageRootFrom } from '../project/package-root.mjs'
|
|
10
|
+
|
|
11
|
+
export const ATELIER_SKILL_AUDIT_SCHEMA = 'mnstry.atelier-skill-audit@v1'
|
|
12
|
+
export const ATELIER_SKILL_CANDIDATES_SCHEMA = 'mnstry.atelier-skill-candidates@v1'
|
|
13
|
+
export const ATELIER_SKILL_SYNC_PLAN_SCHEMA = 'mnstry.atelier-skill-sync-plan@v1'
|
|
14
|
+
export const ATELIER_SKILL_SYNC_LOCK_SCHEMA = 'mnstry.atelier-skill-sync-lock@v1'
|
|
15
|
+
export const SKILL_SYNC_LOCK_FILE = '.atelier-skill-lock.json'
|
|
16
|
+
|
|
17
|
+
export const SKILL_OBSERVATION_SIGNALS = Object.freeze([
|
|
18
|
+
'repeated-task',
|
|
19
|
+
'missing-workflow',
|
|
20
|
+
'user-correction',
|
|
21
|
+
'trigger-miss',
|
|
22
|
+
'trigger-collision',
|
|
23
|
+
'tool-failure',
|
|
24
|
+
'stale-guidance',
|
|
25
|
+
'successful-run',
|
|
26
|
+
'unused-skill',
|
|
27
|
+
'superseded-skill',
|
|
28
|
+
])
|
|
29
|
+
|
|
30
|
+
export const SKILL_OBSERVATION_OUTCOMES = Object.freeze([
|
|
31
|
+
'success',
|
|
32
|
+
'failure',
|
|
33
|
+
'corrected',
|
|
34
|
+
'missing',
|
|
35
|
+
'unknown',
|
|
36
|
+
])
|
|
37
|
+
|
|
38
|
+
const packageRoot = packageRootFrom(import.meta.url)
|
|
39
|
+
const packageJson = JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf8'))
|
|
40
|
+
const stewardSchema = JSON.parse(fs.readFileSync(path.join(packageRoot, 'contracts', 'atelier-skill-steward.v1.schema.json'), 'utf8'))
|
|
41
|
+
const SKILL_NAME_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/
|
|
42
|
+
const WORKFLOW_KEY_PATTERN = /^[a-z0-9][a-z0-9._:-]{0,127}$/
|
|
43
|
+
const DIGEST_PATTERN = /^sha256:[0-9a-f]{64}$/
|
|
44
|
+
const MAX_BUNDLE_FILES = 256
|
|
45
|
+
const MAX_BUNDLE_BYTES = 2 * 1024 * 1024
|
|
46
|
+
const MAX_SKILL_DESCRIPTION = 600
|
|
47
|
+
const OBSERVATION_LEDGER = path.join('.atelier-local', 'skill-steward', 'observations.ndjson')
|
|
48
|
+
|
|
49
|
+
const stableCompare = (left, right) => String(left).localeCompare(String(right), 'en')
|
|
50
|
+
const portablePath = (value) => String(value).split(path.sep).join('/')
|
|
51
|
+
const isObject = (value) => value && typeof value === 'object' && !Array.isArray(value)
|
|
52
|
+
|
|
53
|
+
function sha256(value) {
|
|
54
|
+
return `sha256:${crypto.createHash('sha256').update(value).digest('hex')}`
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function canonicalDigest(value) {
|
|
58
|
+
return sha256(JSON.stringify(value))
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function lstatIfPresent(file) {
|
|
62
|
+
try {
|
|
63
|
+
return fs.lstatSync(file)
|
|
64
|
+
} catch (error) {
|
|
65
|
+
if (error?.code === 'ENOENT') return null
|
|
66
|
+
throw error
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function escaped(root, candidate) {
|
|
71
|
+
const relative = path.relative(root, candidate)
|
|
72
|
+
return relative.startsWith('..') || path.isAbsolute(relative)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function containedPath(root, candidate, label) {
|
|
76
|
+
const resolvedRoot = path.resolve(root)
|
|
77
|
+
const resolved = path.resolve(candidate)
|
|
78
|
+
if (resolved === resolvedRoot || escaped(resolvedRoot, resolved)) throw new Error(`${label} escapes workspace`)
|
|
79
|
+
return resolved
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function relativeTo(root, file) {
|
|
83
|
+
const relative = path.relative(root, file)
|
|
84
|
+
if (relative === '' || escaped(root, file)) return null
|
|
85
|
+
return portablePath(relative)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function strictUtf8(bytes, label) {
|
|
89
|
+
try {
|
|
90
|
+
return new TextDecoder('utf-8', { fatal: true }).decode(bytes)
|
|
91
|
+
} catch {
|
|
92
|
+
throw new Error(`${label} is not valid UTF-8 text`)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function readBounded(file) {
|
|
97
|
+
const fd = openRegularFileNoFollow(file)
|
|
98
|
+
try {
|
|
99
|
+
if (fs.fstatSync(fd).size > MAX_BUNDLE_BYTES) throw new Error('skill file exceeds byte ceiling')
|
|
100
|
+
const bytes = fs.readFileSync(fd)
|
|
101
|
+
if (bytes.length > MAX_BUNDLE_BYTES) throw new Error('skill file exceeds byte ceiling')
|
|
102
|
+
return bytes
|
|
103
|
+
} finally { fs.closeSync(fd) }
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function requirePrivatePlacement(workspaceRoot) {
|
|
107
|
+
const env = Object.fromEntries(Object.entries(process.env).filter(([key]) => !key.toUpperCase().startsWith('GIT_')))
|
|
108
|
+
try {
|
|
109
|
+
const tracked = execFileSync('git', ['-C', workspaceRoot, 'ls-files', '-z', '--', '.atelier-local'], { env, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] })
|
|
110
|
+
if (tracked) throw new Error('tracked private state')
|
|
111
|
+
execFileSync('git', ['-C', workspaceRoot, 'check-ignore', '--quiet', '.atelier-local/'], { env, stdio: 'ignore' })
|
|
112
|
+
} catch { throw new Error('skill stewardship requires untracked, ignored .atelier-local/ state in a Git workspace') }
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function scalarValue(raw) {
|
|
116
|
+
const value = raw.trim()
|
|
117
|
+
if (value.startsWith('"') && value.endsWith('"')) {
|
|
118
|
+
try {
|
|
119
|
+
return JSON.parse(value)
|
|
120
|
+
} catch {
|
|
121
|
+
return value.slice(1, -1)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (value.startsWith("'") && value.endsWith("'")) return value.slice(1, -1).replaceAll("''", "'")
|
|
125
|
+
return value
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function parseSkillFrontmatter(text) {
|
|
129
|
+
const normalized = String(text).replaceAll('\r\n', '\n')
|
|
130
|
+
if (!normalized.startsWith('---\n')) return { ok: false, values: {}, body: normalized, error: 'SKILL.md must begin with YAML frontmatter' }
|
|
131
|
+
const closing = normalized.indexOf('\n---\n', 4)
|
|
132
|
+
if (closing < 0) return { ok: false, values: {}, body: normalized, error: 'SKILL.md frontmatter is not closed' }
|
|
133
|
+
const header = normalized.slice(4, closing)
|
|
134
|
+
const values = {}
|
|
135
|
+
for (const line of header.split('\n')) {
|
|
136
|
+
if (!line || /^\s/.test(line) || line.trimStart().startsWith('#')) continue
|
|
137
|
+
const match = /^([A-Za-z][A-Za-z0-9_-]*):\s*(.*)$/.exec(line)
|
|
138
|
+
if (!match) continue
|
|
139
|
+
values[match[1]] = scalarValue(match[2])
|
|
140
|
+
}
|
|
141
|
+
return { ok: true, values, body: normalized.slice(closing + 5), error: null }
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function bundleFiles(root, relative = '', state = { files: [], bytes: 0, directories: 0 }) {
|
|
145
|
+
if (++state.directories > 256 || relative.split(path.sep).length > 16) throw new Error('skill bundle directory ceiling exceeded')
|
|
146
|
+
const directory = relative ? path.join(root, relative) : root
|
|
147
|
+
const stat = fs.lstatSync(directory)
|
|
148
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) throw new Error('skill bundle contains a redirected or non-directory component')
|
|
149
|
+
for (const entry of fs.readdirSync(directory, { withFileTypes: true }).sort((left, right) => stableCompare(left.name, right.name))) {
|
|
150
|
+
const childRelative = relative ? path.join(relative, entry.name) : entry.name
|
|
151
|
+
const child = path.join(root, childRelative)
|
|
152
|
+
const childStat = fs.lstatSync(child)
|
|
153
|
+
if (childStat.isSymbolicLink()) throw new Error(`skill bundle contains symbolic link ${portablePath(childRelative)}`)
|
|
154
|
+
if (childStat.isDirectory()) {
|
|
155
|
+
bundleFiles(root, childRelative, state)
|
|
156
|
+
continue
|
|
157
|
+
}
|
|
158
|
+
if (!childStat.isFile()) throw new Error(`skill bundle contains non-regular file ${portablePath(childRelative)}`)
|
|
159
|
+
if (state.files.length >= MAX_BUNDLE_FILES) throw new Error(`skill bundle exceeds ${MAX_BUNDLE_FILES} file ceiling`)
|
|
160
|
+
if (state.bytes + childStat.size > MAX_BUNDLE_BYTES) throw new Error(`skill bundle exceeds ${MAX_BUNDLE_BYTES} byte ceiling`)
|
|
161
|
+
const bytes = readBounded(child)
|
|
162
|
+
if (state.bytes + bytes.length > MAX_BUNDLE_BYTES) throw new Error('skill bundle exceeds byte ceiling')
|
|
163
|
+
state.files.push({ path: portablePath(childRelative), bytes })
|
|
164
|
+
state.bytes += bytes.length
|
|
165
|
+
}
|
|
166
|
+
return state
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function bundleDigest(files) {
|
|
170
|
+
const hash = crypto.createHash('sha256')
|
|
171
|
+
for (const file of files) {
|
|
172
|
+
const name = Buffer.from(file.path, 'utf8')
|
|
173
|
+
const size = Buffer.alloc(8)
|
|
174
|
+
size.writeBigUInt64BE(BigInt(file.bytes.length))
|
|
175
|
+
hash.update(Buffer.from(`${name.length}:`, 'utf8'))
|
|
176
|
+
hash.update(name)
|
|
177
|
+
hash.update(size)
|
|
178
|
+
hash.update(file.bytes)
|
|
179
|
+
}
|
|
180
|
+
return `sha256:${hash.digest('hex')}`
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function readSkillBundle(root, directoryName) {
|
|
184
|
+
const skillRoot = path.join(root, directoryName)
|
|
185
|
+
const state = bundleFiles(skillRoot)
|
|
186
|
+
const skillFile = state.files.find((file) => file.path === 'SKILL.md')
|
|
187
|
+
if (!skillFile) throw new Error('skill bundle is missing SKILL.md')
|
|
188
|
+
const skillText = strictUtf8(skillFile.bytes, 'SKILL.md')
|
|
189
|
+
return {
|
|
190
|
+
directoryName,
|
|
191
|
+
root: skillRoot,
|
|
192
|
+
files: state.files,
|
|
193
|
+
bytes: state.bytes,
|
|
194
|
+
digest: bundleDigest(state.files),
|
|
195
|
+
skillText,
|
|
196
|
+
frontmatter: parseSkillFrontmatter(skillText),
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function markdownResourceLinks(text) {
|
|
201
|
+
const links = []
|
|
202
|
+
const pattern = /\[[^\]]*\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g
|
|
203
|
+
for (const match of text.matchAll(pattern)) {
|
|
204
|
+
const target = match[1].replace(/^<|>$/g, '')
|
|
205
|
+
if (!target || target.startsWith('#') || /^[a-z][a-z0-9+.-]*:/i.test(target)) continue
|
|
206
|
+
links.push(target.split('#')[0])
|
|
207
|
+
}
|
|
208
|
+
return [...new Set(links)].sort(stableCompare)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function auditSurface({ name, root, reportRoot }) {
|
|
212
|
+
const entries = []
|
|
213
|
+
const findings = []
|
|
214
|
+
const surfacePath = relativeTo(reportRoot, root) ?? portablePath(path.basename(root))
|
|
215
|
+
const rootStat = lstatIfPresent(root)
|
|
216
|
+
if (!rootStat) {
|
|
217
|
+
findings.push({ severity: 'error', code: 'surface-missing', surface: name, skill: null, path: surfacePath, message: 'skill surface does not exist' })
|
|
218
|
+
return { entries, findings }
|
|
219
|
+
}
|
|
220
|
+
if (rootStat.isSymbolicLink() || !rootStat.isDirectory()) {
|
|
221
|
+
findings.push({ severity: 'error', code: 'surface-redirected', surface: name, skill: null, path: surfacePath, message: 'skill surface must be a real directory' })
|
|
222
|
+
return { entries, findings }
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
for (const directory of fs.readdirSync(root, { withFileTypes: true }).sort((left, right) => stableCompare(left.name, right.name))) {
|
|
226
|
+
if (directory.name.startsWith('.')) continue
|
|
227
|
+
const entryPath = path.join(root, directory.name)
|
|
228
|
+
const entryDisplay = portablePath(path.join(surfacePath, directory.name))
|
|
229
|
+
if (directory.isSymbolicLink()) {
|
|
230
|
+
findings.push({ severity: 'error', code: 'skill-redirected', surface: name, skill: directory.name, path: entryDisplay, message: 'skill entry must not be a symbolic link' })
|
|
231
|
+
continue
|
|
232
|
+
}
|
|
233
|
+
if (!directory.isDirectory()) {
|
|
234
|
+
findings.push({ severity: 'warning', code: 'unexpected-surface-file', surface: name, skill: null, path: entryDisplay, message: 'non-directory entry is ignored' })
|
|
235
|
+
continue
|
|
236
|
+
}
|
|
237
|
+
let bundle
|
|
238
|
+
try {
|
|
239
|
+
bundle = readSkillBundle(root, directory.name)
|
|
240
|
+
} catch (error) {
|
|
241
|
+
findings.push({ severity: 'error', code: 'skill-bundle-invalid', surface: name, skill: directory.name, path: entryDisplay, message: error.message })
|
|
242
|
+
continue
|
|
243
|
+
}
|
|
244
|
+
const { frontmatter } = bundle
|
|
245
|
+
if (!frontmatter.ok) {
|
|
246
|
+
findings.push({ severity: 'error', code: 'frontmatter-invalid', surface: name, skill: directory.name, path: `${entryDisplay}/SKILL.md`, message: frontmatter.error })
|
|
247
|
+
}
|
|
248
|
+
const declaredName = typeof frontmatter.values.name === 'string' ? frontmatter.values.name.trim() : ''
|
|
249
|
+
const description = typeof frontmatter.values.description === 'string' ? frontmatter.values.description.trim() : ''
|
|
250
|
+
if (!declaredName) findings.push({ severity: 'error', code: 'name-missing', surface: name, skill: directory.name, path: `${entryDisplay}/SKILL.md`, message: 'frontmatter name is required' })
|
|
251
|
+
else {
|
|
252
|
+
if (!SKILL_NAME_PATTERN.test(declaredName) || declaredName.length > 64) {
|
|
253
|
+
findings.push({ severity: 'error', code: 'name-invalid', surface: name, skill: directory.name, path: `${entryDisplay}/SKILL.md`, message: 'frontmatter name must be lowercase kebab-case and at most 64 characters' })
|
|
254
|
+
}
|
|
255
|
+
if (declaredName !== directory.name) {
|
|
256
|
+
findings.push({ severity: 'error', code: 'name-directory-mismatch', surface: name, skill: directory.name, path: `${entryDisplay}/SKILL.md`, message: 'frontmatter name must match its directory' })
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
if (!description) findings.push({ severity: 'error', code: 'description-missing', surface: name, skill: directory.name, path: `${entryDisplay}/SKILL.md`, message: 'frontmatter description is required' })
|
|
260
|
+
else if (description.length > MAX_SKILL_DESCRIPTION) {
|
|
261
|
+
findings.push({ severity: 'warning', code: 'description-long', surface: name, skill: directory.name, path: `${entryDisplay}/SKILL.md`, message: `frontmatter description exceeds ${MAX_SKILL_DESCRIPTION} characters` })
|
|
262
|
+
}
|
|
263
|
+
const fileNames = new Set(bundle.files.map((file) => file.path))
|
|
264
|
+
for (const link of markdownResourceLinks(bundle.skillText)) {
|
|
265
|
+
const normalized = portablePath(path.posix.normalize(link.replaceAll('\\', '/')))
|
|
266
|
+
if (normalized === '..' || normalized.startsWith('../')) {
|
|
267
|
+
findings.push({ severity: 'error', code: 'reference-escapes-bundle', surface: name, skill: directory.name, path: `${entryDisplay}/SKILL.md`, message: 'relative resource link escapes the skill bundle' })
|
|
268
|
+
} else if (!fileNames.has(normalized)) {
|
|
269
|
+
findings.push({ severity: 'error', code: 'reference-missing', surface: name, skill: directory.name, path: `${entryDisplay}/SKILL.md`, message: `relative resource link is missing: ${normalized}` })
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
if (/\b(?:TODO|TBD|REPLACE_ME)\b/.test(bundle.skillText)) {
|
|
273
|
+
findings.push({ severity: 'warning', code: 'unfinished-placeholder', surface: name, skill: directory.name, path: `${entryDisplay}/SKILL.md`, message: 'skill contains an unfinished scaffold placeholder' })
|
|
274
|
+
}
|
|
275
|
+
const entryName = SKILL_NAME_PATTERN.test(declaredName) && declaredName.length <= 64
|
|
276
|
+
? declaredName
|
|
277
|
+
: SKILL_NAME_PATTERN.test(directory.name) && directory.name.length <= 64
|
|
278
|
+
? directory.name
|
|
279
|
+
: null
|
|
280
|
+
if (entryName) {
|
|
281
|
+
entries.push({
|
|
282
|
+
surface: name,
|
|
283
|
+
name: entryName,
|
|
284
|
+
description,
|
|
285
|
+
lifecycle: 'active',
|
|
286
|
+
risk: bundle.files.length === 1 ? 'instruction-only' : 'resource-backed',
|
|
287
|
+
digest: bundle.digest,
|
|
288
|
+
fileCount: bundle.files.length,
|
|
289
|
+
bytes: bundle.bytes,
|
|
290
|
+
})
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
return { entries, findings }
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function parityFindings(entries, surfaces) {
|
|
297
|
+
if (surfaces.length < 2) return []
|
|
298
|
+
const findings = []
|
|
299
|
+
const canonical = surfaces[0].name
|
|
300
|
+
const bySurface = new Map(surfaces.map((surface) => [surface.name, new Map()]))
|
|
301
|
+
for (const entry of entries) bySurface.get(entry.surface)?.set(entry.name, entry)
|
|
302
|
+
const names = [...new Set(entries.map((entry) => entry.name))].sort(stableCompare)
|
|
303
|
+
for (const name of names) {
|
|
304
|
+
const base = bySurface.get(canonical)?.get(name) ?? null
|
|
305
|
+
for (const surface of surfaces.slice(1)) {
|
|
306
|
+
const peer = bySurface.get(surface.name)?.get(name) ?? null
|
|
307
|
+
if (!base) {
|
|
308
|
+
findings.push({ severity: 'error', code: 'peer-only-skill', surface: surface.name, skill: name, path: name, message: `skill is absent from canonical surface ${canonical}` })
|
|
309
|
+
} else if (!peer) {
|
|
310
|
+
findings.push({ severity: 'error', code: 'peer-missing-skill', surface: surface.name, skill: name, path: name, message: `skill is missing from peer surface ${surface.name}` })
|
|
311
|
+
} else if (base.digest !== peer.digest) {
|
|
312
|
+
findings.push({ severity: 'error', code: 'peer-bundle-mismatch', surface: surface.name, skill: name, path: name, message: `skill bundle differs from canonical surface ${canonical}` })
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
return findings
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function reportAuthority() {
|
|
320
|
+
return {
|
|
321
|
+
telemetry: false,
|
|
322
|
+
egress: false,
|
|
323
|
+
promptCapture: false,
|
|
324
|
+
sourceMutation: false,
|
|
325
|
+
runtimeMutation: false,
|
|
326
|
+
applyEndpoint: null,
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
export function auditSkillCatalog({ surfaces, reportRoot = process.cwd(), clock = () => new Date().toISOString() } = {}) {
|
|
331
|
+
const selected = Array.isArray(surfaces) && surfaces.length
|
|
332
|
+
? surfaces.map((surface, index) => ({ name: String(surface.name || `surface-${index + 1}`), root: path.resolve(surface.root) }))
|
|
333
|
+
: [
|
|
334
|
+
{ name: 'codex', root: path.join(packageRoot, 'skills', 'codex') },
|
|
335
|
+
{ name: 'claude', root: path.join(packageRoot, 'skills', 'claude') },
|
|
336
|
+
]
|
|
337
|
+
const entries = []
|
|
338
|
+
const findings = []
|
|
339
|
+
for (const surface of selected) {
|
|
340
|
+
const audited = auditSurface({ ...surface, reportRoot: path.resolve(reportRoot) })
|
|
341
|
+
entries.push(...audited.entries)
|
|
342
|
+
findings.push(...audited.findings)
|
|
343
|
+
}
|
|
344
|
+
findings.push(...parityFindings(entries, selected))
|
|
345
|
+
entries.sort((left, right) => stableCompare(`${left.surface}:${left.name}`, `${right.surface}:${right.name}`))
|
|
346
|
+
findings.sort((left, right) => stableCompare(`${left.severity}:${left.surface}:${left.skill}:${left.code}`, `${right.severity}:${right.surface}:${right.skill}:${right.code}`))
|
|
347
|
+
const errors = findings.filter((finding) => finding.severity === 'error').length
|
|
348
|
+
const warnings = findings.filter((finding) => finding.severity === 'warning').length
|
|
349
|
+
const report = {
|
|
350
|
+
schema: ATELIER_SKILL_AUDIT_SCHEMA,
|
|
351
|
+
generatedAt: clock(),
|
|
352
|
+
ok: errors === 0,
|
|
353
|
+
surfaces: selected.map((surface) => ({ name: surface.name, path: relativeTo(path.resolve(reportRoot), surface.root) ?? portablePath(path.basename(surface.root)) })),
|
|
354
|
+
summary: { skills: entries.length, errors, warnings },
|
|
355
|
+
entries,
|
|
356
|
+
findings,
|
|
357
|
+
authority: reportAuthority(),
|
|
358
|
+
}
|
|
359
|
+
const validation = validateSkillStewardDocument(report, '#/$defs/auditReport')
|
|
360
|
+
if (validation.length) throw new Error(`generated skill audit violates its contract: ${validation.join('; ')}`)
|
|
361
|
+
return report
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function skillObservationLedger(workspaceRoot) {
|
|
365
|
+
requirePrivatePlacement(workspaceRoot)
|
|
366
|
+
return createCollaborationEventLedger({
|
|
367
|
+
workspaceRoot,
|
|
368
|
+
ledgerPath: path.join(workspaceRoot, OBSERVATION_LEDGER),
|
|
369
|
+
})
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function validateObservation({ workflowKey, signal, skill, outcome }) {
|
|
373
|
+
const errors = []
|
|
374
|
+
if (typeof workflowKey !== 'string' || !WORKFLOW_KEY_PATTERN.test(workflowKey)) errors.push('workflow key is invalid')
|
|
375
|
+
if (!SKILL_OBSERVATION_SIGNALS.includes(signal)) errors.push(`signal must be one of ${SKILL_OBSERVATION_SIGNALS.join(', ')}`)
|
|
376
|
+
if (skill != null && (typeof skill !== 'string' || skill.length > 64 || !SKILL_NAME_PATTERN.test(skill))) errors.push('skill name is invalid')
|
|
377
|
+
if (!SKILL_OBSERVATION_OUTCOMES.includes(outcome)) errors.push(`outcome must be one of ${SKILL_OBSERVATION_OUTCOMES.join(', ')}`)
|
|
378
|
+
return errors
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
export function recordSkillObservation({
|
|
382
|
+
workspaceRoot = process.cwd(),
|
|
383
|
+
workflowKey,
|
|
384
|
+
signal,
|
|
385
|
+
skill = null,
|
|
386
|
+
outcome = 'unknown',
|
|
387
|
+
at = new Date().toISOString(),
|
|
388
|
+
...unknown
|
|
389
|
+
} = {}) {
|
|
390
|
+
if (Object.keys(unknown).length) throw new Error('unknown skill observation fields refused')
|
|
391
|
+
const errors = validateObservation({ workflowKey, signal, skill, outcome })
|
|
392
|
+
if (errors.length) throw new Error(errors.join('; '))
|
|
393
|
+
const ledger = skillObservationLedger(workspaceRoot)
|
|
394
|
+
const current = ledger.eventsFor(workflowKey)
|
|
395
|
+
if (!current.ok) throw new Error(current.error)
|
|
396
|
+
const result = ledger.append({
|
|
397
|
+
aggregateId: workflowKey,
|
|
398
|
+
expectedVersion: current.currentVersion,
|
|
399
|
+
type: `skill-observation.${signal}`,
|
|
400
|
+
actor: 'atelier-skill-steward',
|
|
401
|
+
at,
|
|
402
|
+
payload: { signal, skill, outcome },
|
|
403
|
+
})
|
|
404
|
+
if (!result.ok) throw new Error(result.error)
|
|
405
|
+
return {
|
|
406
|
+
ok: true,
|
|
407
|
+
schema: 'mnstry.atelier-skill-observation-receipt@v1',
|
|
408
|
+
observation: {
|
|
409
|
+
id: result.event.id,
|
|
410
|
+
workflowKey,
|
|
411
|
+
version: result.event.version,
|
|
412
|
+
signal,
|
|
413
|
+
skill,
|
|
414
|
+
outcome,
|
|
415
|
+
observedAt: result.event.at,
|
|
416
|
+
},
|
|
417
|
+
storage: portablePath(OBSERVATION_LEDGER),
|
|
418
|
+
authority: { telemetry: false, egress: false, promptCapture: false },
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function candidateKind(events) {
|
|
423
|
+
const lastSkill = [...events].reverse().find((event) => typeof event.payload?.skill === 'string')?.payload.skill ?? null
|
|
424
|
+
const counts = Object.fromEntries(SKILL_OBSERVATION_SIGNALS.map((signal) => [signal, 0]))
|
|
425
|
+
for (const event of events) {
|
|
426
|
+
if (Object.hasOwn(counts, event.payload?.signal)) counts[event.payload.signal] += 1
|
|
427
|
+
}
|
|
428
|
+
const creationEvidence = counts['repeated-task'] + counts['missing-workflow']
|
|
429
|
+
const improvementEvidence = counts['user-correction'] + counts['trigger-miss'] + counts['tool-failure'] + counts['stale-guidance']
|
|
430
|
+
const collisionEvidence = counts['trigger-collision']
|
|
431
|
+
let kind = null
|
|
432
|
+
let eligible = false
|
|
433
|
+
if (collisionEvidence >= 2) {
|
|
434
|
+
kind = 'reconcile'
|
|
435
|
+
eligible = true
|
|
436
|
+
} else if (lastSkill && (counts['superseded-skill'] >= 2 || counts['unused-skill'] >= 3)) {
|
|
437
|
+
kind = 'retire'
|
|
438
|
+
eligible = true
|
|
439
|
+
} else if (lastSkill && (counts['user-correction'] >= 2 || improvementEvidence >= 3)) {
|
|
440
|
+
kind = 'improve'
|
|
441
|
+
eligible = true
|
|
442
|
+
} else if (!lastSkill && creationEvidence >= 3) {
|
|
443
|
+
kind = 'create'
|
|
444
|
+
eligible = true
|
|
445
|
+
}
|
|
446
|
+
return { kind, eligible, skill: lastSkill, counts }
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
export function buildSkillCandidates({ workspaceRoot = process.cwd(), includeInsufficient = false, clock = () => new Date().toISOString() } = {}) {
|
|
450
|
+
const ledger = skillObservationLedger(workspaceRoot)
|
|
451
|
+
const result = ledger.readAll()
|
|
452
|
+
if (!result.ok) throw new Error(result.error)
|
|
453
|
+
const groups = new Map()
|
|
454
|
+
for (const event of result.events) {
|
|
455
|
+
if (!event.type.startsWith('skill-observation.')) continue
|
|
456
|
+
if (validateObservation({ workflowKey: event.aggregateId, ...event.payload }).length || event.type !== `skill-observation.${event.payload.signal}`) throw new Error('invalid stored skill observation')
|
|
457
|
+
const key = JSON.stringify([event.aggregateId, event.payload.skill ?? null])
|
|
458
|
+
const group = groups.get(key) ?? []
|
|
459
|
+
group.push(event)
|
|
460
|
+
groups.set(key, group)
|
|
461
|
+
}
|
|
462
|
+
const candidates = []
|
|
463
|
+
for (const [key, events] of [...groups.entries()].sort(([left], [right]) => stableCompare(left, right))) {
|
|
464
|
+
const [workflowKey] = JSON.parse(key)
|
|
465
|
+
const classified = candidateKind(events)
|
|
466
|
+
if (!classified.eligible && !includeInsufficient) continue
|
|
467
|
+
candidates.push({
|
|
468
|
+
workflowKey,
|
|
469
|
+
kind: classified.kind ?? 'observe',
|
|
470
|
+
status: classified.eligible ? 'eligible' : 'insufficient-evidence',
|
|
471
|
+
skill: classified.skill,
|
|
472
|
+
evidenceCount: events.length,
|
|
473
|
+
signals: classified.counts,
|
|
474
|
+
lastObservedAt: events.at(-1)?.at ?? null,
|
|
475
|
+
})
|
|
476
|
+
}
|
|
477
|
+
const report = {
|
|
478
|
+
schema: ATELIER_SKILL_CANDIDATES_SCHEMA,
|
|
479
|
+
generatedAt: clock(),
|
|
480
|
+
ok: true,
|
|
481
|
+
candidates,
|
|
482
|
+
observationCount: result.events.length,
|
|
483
|
+
diagnostics: [],
|
|
484
|
+
authority: reportAuthority(),
|
|
485
|
+
}
|
|
486
|
+
const validation = validateSkillStewardDocument(report, '#/$defs/candidateReport')
|
|
487
|
+
if (validation.length) throw new Error(`generated skill candidates violate their contract: ${validation.join('; ')}`)
|
|
488
|
+
return report
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function catalogEntries(sourceRoot) {
|
|
492
|
+
const audit = auditSkillCatalog({ surfaces: [{ name: 'source', root: sourceRoot }], reportRoot: path.dirname(sourceRoot) })
|
|
493
|
+
return { audit, entries: audit.entries.map((entry) => ({ name: entry.name, digest: entry.digest })).sort((left, right) => stableCompare(left.name, right.name)) }
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function syncAuthority() {
|
|
497
|
+
return {
|
|
498
|
+
telemetry: false,
|
|
499
|
+
egress: false,
|
|
500
|
+
sourceMutation: false,
|
|
501
|
+
targetMutation: 'content-bound-confirmed',
|
|
502
|
+
destructiveDelete: false,
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function readSyncLock(lockPath) {
|
|
507
|
+
const stat = lstatIfPresent(lockPath)
|
|
508
|
+
if (!stat) return { lock: null, errors: [] }
|
|
509
|
+
if (stat.isSymbolicLink() || !stat.isFile()) return { lock: null, errors: ['skill sync lock must be a regular file'] }
|
|
510
|
+
let lock
|
|
511
|
+
try {
|
|
512
|
+
lock = JSON.parse(strictUtf8(readBounded(lockPath), 'skill sync lock'))
|
|
513
|
+
} catch (error) {
|
|
514
|
+
return { lock: null, errors: [`skill sync lock cannot be read: ${error.message}`] }
|
|
515
|
+
}
|
|
516
|
+
const errors = validateSkillStewardDocument(lock, '#/$defs/syncLock').map((error) => `skill sync lock ${error}`)
|
|
517
|
+
if (errors.length) return { lock: null, errors }
|
|
518
|
+
const names = (lock?.skills ?? []).map((entry) => entry.name)
|
|
519
|
+
if (new Set(names).size !== names.length) errors.push('skill sync lock contains duplicate skill names')
|
|
520
|
+
return { lock, errors }
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
function currentSkillDigest(targetRoot, name) {
|
|
524
|
+
const stat = lstatIfPresent(path.join(targetRoot, name))
|
|
525
|
+
if (!stat) return null
|
|
526
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) throw new Error(`target skill ${name} must be a real directory`)
|
|
527
|
+
return readSkillBundle(targetRoot, name).digest
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function verifyDirectoryPath(workspace, directory) {
|
|
531
|
+
const relative = path.relative(workspace, directory)
|
|
532
|
+
let current = workspace
|
|
533
|
+
for (const segment of relative.split(path.sep)) {
|
|
534
|
+
current = path.join(current, segment)
|
|
535
|
+
const stat = lstatIfPresent(current)
|
|
536
|
+
if (!stat) return
|
|
537
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) {
|
|
538
|
+
throw new Error('skill sync target contains a redirected or non-directory component')
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
export function planSkillSync({
|
|
544
|
+
workspaceRoot = process.cwd(),
|
|
545
|
+
sourceRoot = path.join(packageRoot, 'skills', 'codex'),
|
|
546
|
+
target = path.join('.agents', 'skills'),
|
|
547
|
+
clock = () => new Date().toISOString(),
|
|
548
|
+
} = {}) {
|
|
549
|
+
const workspace = fs.realpathSync(path.resolve(workspaceRoot))
|
|
550
|
+
const targetRoot = containedPath(workspace, path.resolve(workspace, target), 'skill sync target')
|
|
551
|
+
const sourcePath = fs.realpathSync(path.resolve(sourceRoot))
|
|
552
|
+
if (sourcePath === targetRoot || !escaped(sourcePath, targetRoot) || !escaped(targetRoot, sourcePath)) throw new Error('skill source and target must not overlap')
|
|
553
|
+
const privatePath = path.join(workspace, '.atelier-local')
|
|
554
|
+
if (targetRoot === privatePath || !escaped(privatePath, targetRoot)) throw new Error('skill target must not overlap private state')
|
|
555
|
+
verifyDirectoryPath(workspace, targetRoot)
|
|
556
|
+
const targetRelative = portablePath(path.relative(workspace, targetRoot))
|
|
557
|
+
if (targetRelative.split('/').includes('.git')) throw new Error('skill target must not overlap Git metadata')
|
|
558
|
+
const lockPath = path.join(targetRoot, SKILL_SYNC_LOCK_FILE)
|
|
559
|
+
const lockRelative = portablePath(path.relative(workspace, lockPath))
|
|
560
|
+
const blockers = []
|
|
561
|
+
let source
|
|
562
|
+
try {
|
|
563
|
+
source = catalogEntries(path.resolve(sourceRoot))
|
|
564
|
+
} catch (error) {
|
|
565
|
+
source = { audit: null, entries: [] }
|
|
566
|
+
blockers.push(`source catalog cannot be audited: ${error.message}`)
|
|
567
|
+
}
|
|
568
|
+
if (source.audit && !source.audit.ok) blockers.push(...source.audit.findings.filter((finding) => finding.severity === 'error').map((finding) => `${finding.skill ?? finding.surface}: ${finding.message}`))
|
|
569
|
+
const lockRead = readSyncLock(lockPath)
|
|
570
|
+
blockers.push(...lockRead.errors)
|
|
571
|
+
const lock = lockRead.lock
|
|
572
|
+
const locked = new Map((lock?.skills ?? []).map((entry) => [entry.name, entry.digest]))
|
|
573
|
+
const next = new Map(source.entries.map((entry) => [entry.name, entry.digest]))
|
|
574
|
+
const names = [...new Set([...next.keys(), ...locked.keys()])].sort(stableCompare)
|
|
575
|
+
const actions = []
|
|
576
|
+
const planToken = canonicalDigest({ source: source.entries, target: targetRelative }).slice('sha256:'.length, 'sha256:'.length + 16)
|
|
577
|
+
for (const name of names) {
|
|
578
|
+
const expectedDigest = locked.get(name) ?? null
|
|
579
|
+
const nextDigest = next.get(name) ?? null
|
|
580
|
+
let currentDigest = null
|
|
581
|
+
try {
|
|
582
|
+
currentDigest = currentSkillDigest(targetRoot, name)
|
|
583
|
+
} catch (error) {
|
|
584
|
+
blockers.push(error.message)
|
|
585
|
+
continue
|
|
586
|
+
}
|
|
587
|
+
if (expectedDigest && expectedDigest !== currentDigest) {
|
|
588
|
+
blockers.push(`managed target skill ${name} has local drift; preserve or review it before sync`)
|
|
589
|
+
continue
|
|
590
|
+
}
|
|
591
|
+
if (!nextDigest && expectedDigest) {
|
|
592
|
+
if (!currentDigest) continue
|
|
593
|
+
actions.push({ type: 'quarantine', name, expectedDigest, currentDigest, nextDigest: null, quarantinePath: portablePath(path.join('.atelier-local', 'skill-steward', 'quarantine', planToken, name)) })
|
|
594
|
+
} else if (nextDigest && !currentDigest) {
|
|
595
|
+
actions.push({ type: 'add', name, expectedDigest: null, currentDigest: null, nextDigest, quarantinePath: null })
|
|
596
|
+
} else if (nextDigest && currentDigest && !expectedDigest) {
|
|
597
|
+
blockers.push(`target skill ${name} already exists and is not managed by Atelier`)
|
|
598
|
+
} else if (nextDigest && currentDigest && nextDigest === currentDigest) {
|
|
599
|
+
actions.push({ type: 'unchanged', name, expectedDigest, currentDigest, nextDigest, quarantinePath: null })
|
|
600
|
+
} else if (nextDigest && currentDigest) {
|
|
601
|
+
actions.push({ type: 'update', name, expectedDigest, currentDigest, nextDigest, quarantinePath: portablePath(path.join('.atelier-local', 'skill-steward', 'quarantine', planToken, name)) })
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
actions.sort((left, right) => stableCompare(left.name, right.name))
|
|
605
|
+
blockers.sort(stableCompare)
|
|
606
|
+
const catalogDigest = canonicalDigest(source.entries)
|
|
607
|
+
const authoritative = {
|
|
608
|
+
source: { packageName: packageJson.name, packageVersion: packageJson.version, catalogDigest,
|
|
609
|
+
ext: { catalogOrigin: sourcePath === path.join(packageRoot, 'skills', 'codex') ? 'bundled-codex' : 'caller-selected' } },
|
|
610
|
+
target: { path: targetRelative, lockPath: lockRelative },
|
|
611
|
+
actions,
|
|
612
|
+
blockers,
|
|
613
|
+
authority: syncAuthority(),
|
|
614
|
+
ext: { workspaceBinding: sha256(workspace), sourceBinding: sha256(sourcePath) },
|
|
615
|
+
}
|
|
616
|
+
const plan = {
|
|
617
|
+
schema: ATELIER_SKILL_SYNC_PLAN_SCHEMA,
|
|
618
|
+
generatedAt: clock(),
|
|
619
|
+
planDigest: canonicalDigest(authoritative),
|
|
620
|
+
applyAllowed: blockers.length === 0,
|
|
621
|
+
...authoritative,
|
|
622
|
+
}
|
|
623
|
+
const validation = validateSkillStewardDocument(plan, '#/$defs/syncPlan')
|
|
624
|
+
if (validation.length) throw new Error(`generated skill sync plan violates its contract: ${validation.join('; ')}`)
|
|
625
|
+
return plan
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
function copyBundle(sourceRoot, targetRoot, name, expectedDigest) {
|
|
629
|
+
const source = path.join(sourceRoot, name)
|
|
630
|
+
const target = path.join(targetRoot, name)
|
|
631
|
+
fs.mkdirSync(target, { recursive: false, mode: 0o700 })
|
|
632
|
+
const bundle = readSkillBundle(sourceRoot, name)
|
|
633
|
+
for (const file of bundle.files) {
|
|
634
|
+
const output = path.join(target, ...file.path.split('/'))
|
|
635
|
+
fs.mkdirSync(path.dirname(output), { recursive: true, mode: 0o700 })
|
|
636
|
+
fs.writeFileSync(output, file.bytes, { mode: 0o600, flag: 'wx' })
|
|
637
|
+
}
|
|
638
|
+
const copied = readSkillBundle(targetRoot, name)
|
|
639
|
+
if (copied.digest !== bundle.digest) throw new Error(`staged skill ${name} digest mismatch`)
|
|
640
|
+
if (copied.digest !== expectedDigest) throw new Error(`source skill ${name} changed after the confirmed plan was prepared`)
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
function ensureTargetDirectory(workspace, targetRoot) {
|
|
644
|
+
const relative = path.relative(workspace, targetRoot)
|
|
645
|
+
let current = workspace
|
|
646
|
+
for (const segment of relative.split(path.sep)) {
|
|
647
|
+
current = path.join(current, segment)
|
|
648
|
+
let stat = lstatIfPresent(current)
|
|
649
|
+
if (!stat) {
|
|
650
|
+
fs.mkdirSync(current, { mode: 0o700 })
|
|
651
|
+
stat = fs.lstatSync(current)
|
|
652
|
+
}
|
|
653
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) throw new Error('skill sync target contains a redirected or non-directory component')
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
function applySkillSyncLocked({
|
|
658
|
+
workspaceRoot = process.cwd(),
|
|
659
|
+
sourceRoot = path.join(packageRoot, 'skills', 'codex'),
|
|
660
|
+
target = path.join('.agents', 'skills'),
|
|
661
|
+
confirm,
|
|
662
|
+
clock = () => new Date().toISOString(),
|
|
663
|
+
} = {}) {
|
|
664
|
+
const workspace = fs.realpathSync(path.resolve(workspaceRoot))
|
|
665
|
+
const targetRoot = containedPath(workspace, path.resolve(workspace, target), 'skill sync target')
|
|
666
|
+
const plan = planSkillSync({ workspaceRoot: workspace, sourceRoot, target, clock })
|
|
667
|
+
if (!DIGEST_PATTERN.test(String(confirm ?? '')) || confirm !== plan.planDigest) throw new Error('skill sync confirmation must exactly match the current plan digest')
|
|
668
|
+
if (!plan.applyAllowed) throw new Error(plan.blockers.join('\n'))
|
|
669
|
+
ensureTargetDirectory(workspace, targetRoot)
|
|
670
|
+
|
|
671
|
+
const planKey = plan.actions.find((action) => action.quarantinePath)?.quarantinePath.split('/').at(-2) ?? plan.planDigest.slice('sha256:'.length, 'sha256:'.length + 16)
|
|
672
|
+
const privateRoot = ensureContainedPrivateDirectory({ workspaceRoot: workspace, directory: path.join(workspace, '.atelier-local', 'skill-steward'), label: 'skill steward private state' })
|
|
673
|
+
const stagingRoot = ensureContainedPrivateDirectory({ workspaceRoot: workspace, directory: path.join(privateRoot, 'staging', planKey), label: 'skill sync staging directory' })
|
|
674
|
+
const quarantineRoot = ensureContainedPrivateDirectory({ workspaceRoot: workspace, directory: path.join(privateRoot, 'quarantine', planKey), label: 'skill sync quarantine directory' })
|
|
675
|
+
if (fs.readdirSync(stagingRoot).length || fs.readdirSync(quarantineRoot).length) throw new Error('skill sync plan state already exists; inspect or clear the preserved state before retrying')
|
|
676
|
+
|
|
677
|
+
const mutating = plan.actions.filter((action) => ['add', 'update'].includes(action.type))
|
|
678
|
+
try {
|
|
679
|
+
for (const action of mutating) copyBundle(path.resolve(sourceRoot), stagingRoot, action.name, action.nextDigest)
|
|
680
|
+
} catch (error) {
|
|
681
|
+
throw new Error(`skill sync staging failed: ${error.message}`)
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
const applied = []
|
|
685
|
+
try {
|
|
686
|
+
verifyDirectoryPath(workspace, targetRoot)
|
|
687
|
+
if (planSkillSync({ workspaceRoot: workspace, sourceRoot, target, clock }).planDigest !== plan.planDigest) throw new Error('skill sync plan changed during staging')
|
|
688
|
+
for (const action of plan.actions) {
|
|
689
|
+
const currentDigest = currentSkillDigest(targetRoot, action.name)
|
|
690
|
+
if (currentDigest !== action.currentDigest) throw new Error(`target skill ${action.name} changed after the confirmed plan was prepared`)
|
|
691
|
+
}
|
|
692
|
+
for (const action of plan.actions) {
|
|
693
|
+
const targetSkill = path.join(targetRoot, action.name)
|
|
694
|
+
const stagedSkill = path.join(stagingRoot, action.name)
|
|
695
|
+
const quarantinedSkill = path.join(quarantineRoot, action.name)
|
|
696
|
+
if (action.type === 'add') {
|
|
697
|
+
fs.renameSync(stagedSkill, targetSkill)
|
|
698
|
+
applied.push({ action, targetSkill, stagedSkill, quarantinedSkill })
|
|
699
|
+
} else if (action.type === 'update') {
|
|
700
|
+
fs.renameSync(targetSkill, quarantinedSkill)
|
|
701
|
+
try {
|
|
702
|
+
fs.renameSync(stagedSkill, targetSkill)
|
|
703
|
+
} catch (error) {
|
|
704
|
+
fs.renameSync(quarantinedSkill, targetSkill)
|
|
705
|
+
throw error
|
|
706
|
+
}
|
|
707
|
+
applied.push({ action, targetSkill, stagedSkill, quarantinedSkill })
|
|
708
|
+
} else if (action.type === 'quarantine') {
|
|
709
|
+
fs.renameSync(targetSkill, quarantinedSkill)
|
|
710
|
+
applied.push({ action, targetSkill, stagedSkill, quarantinedSkill })
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
const lock = {
|
|
715
|
+
schema: ATELIER_SKILL_SYNC_LOCK_SCHEMA,
|
|
716
|
+
generatedAt: clock(),
|
|
717
|
+
source: plan.source,
|
|
718
|
+
target: plan.target.path,
|
|
719
|
+
skills: plan.actions
|
|
720
|
+
.filter((action) => action.nextDigest)
|
|
721
|
+
.map((action) => ({ name: action.name, digest: action.nextDigest }))
|
|
722
|
+
.sort((left, right) => stableCompare(left.name, right.name)),
|
|
723
|
+
authority: syncAuthority(),
|
|
724
|
+
}
|
|
725
|
+
const validation = validateSkillStewardDocument(lock, '#/$defs/syncLock')
|
|
726
|
+
if (validation.length) throw new Error(`generated skill sync lock violates its contract: ${validation.join('; ')}`)
|
|
727
|
+
atomicReplacePrivateText(path.join(targetRoot, SKILL_SYNC_LOCK_FILE), `${JSON.stringify(lock, null, 2)}\n`, 0o600)
|
|
728
|
+
try {
|
|
729
|
+
fs.rmSync(stagingRoot, { recursive: true })
|
|
730
|
+
} catch {
|
|
731
|
+
// The applied projection is authoritative; leftover empty private state is diagnosable and ignored.
|
|
732
|
+
}
|
|
733
|
+
return {
|
|
734
|
+
ok: true,
|
|
735
|
+
schema: 'mnstry.atelier-skill-sync-result@v1',
|
|
736
|
+
plan,
|
|
737
|
+
lock,
|
|
738
|
+
quarantined: applied.filter((item) => ['update', 'quarantine'].includes(item.action.type)).map((item) => portablePath(path.relative(workspace, item.quarantinedSkill))),
|
|
739
|
+
}
|
|
740
|
+
} catch (error) {
|
|
741
|
+
for (const item of [...applied].reverse()) {
|
|
742
|
+
try {
|
|
743
|
+
if (item.action.type === 'add' && lstatIfPresent(item.targetSkill)) fs.renameSync(item.targetSkill, item.stagedSkill)
|
|
744
|
+
if (item.action.type === 'update') {
|
|
745
|
+
if (lstatIfPresent(item.targetSkill)) fs.renameSync(item.targetSkill, item.stagedSkill)
|
|
746
|
+
if (lstatIfPresent(item.quarantinedSkill)) fs.renameSync(item.quarantinedSkill, item.targetSkill)
|
|
747
|
+
}
|
|
748
|
+
if (item.action.type === 'quarantine' && lstatIfPresent(item.quarantinedSkill)) fs.renameSync(item.quarantinedSkill, item.targetSkill)
|
|
749
|
+
} catch {
|
|
750
|
+
// Preserve every directory for manual recovery; never delete ambiguous state.
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
throw new Error(`skill sync apply failed and preserved recovery state: ${error.message}`)
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
export function applySkillSync(options = {}) {
|
|
758
|
+
const workspace = fs.realpathSync(path.resolve(options.workspaceRoot ?? process.cwd()))
|
|
759
|
+
// Confirmation is checked before creating private operation state.
|
|
760
|
+
const plan = planSkillSync(options)
|
|
761
|
+
if (options.confirm !== plan.planDigest) throw new Error('skill sync confirmation must exactly match the current plan digest')
|
|
762
|
+
if (!plan.applyAllowed) throw new Error(plan.blockers.join('\n'))
|
|
763
|
+
requirePrivatePlacement(workspace)
|
|
764
|
+
const directory = ensureContainedPrivateDirectory({ workspaceRoot: workspace, directory: path.join(workspace, '.atelier-local', 'skill-steward'), label: 'skill steward private state' })
|
|
765
|
+
const lockPath = path.join(directory, '.operation.lock')
|
|
766
|
+
const descriptor = fs.openSync(lockPath, fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY | fs.constants.O_NOFOLLOW, 0o600)
|
|
767
|
+
try {
|
|
768
|
+
return applySkillSyncLocked(options)
|
|
769
|
+
} finally {
|
|
770
|
+
fs.closeSync(descriptor)
|
|
771
|
+
fs.unlinkSync(lockPath)
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
export function validateSkillStewardDocument(document, pointer = null) {
|
|
776
|
+
const schema = pointer
|
|
777
|
+
? { $schema: stewardSchema.$schema, $defs: stewardSchema.$defs, $ref: pointer }
|
|
778
|
+
: stewardSchema
|
|
779
|
+
return validateJsonSchema(schema, document)
|
|
780
|
+
}
|