@mnstry/atelier 0.2.0-alpha.1 → 0.2.0-alpha.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +413 -0
- package/README.md +402 -205
- 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/contracts/public-api-baseline.json +57 -0
- package/docs/assurance-controls.md +49 -0
- package/docs/atelier-runtime.md +28 -2
- package/docs/atelier-sync.md +172 -0
- package/docs/blocks/audience-visibility.md +9 -0
- package/docs/blocks/claims.md +61 -0
- package/docs/blocks/conformance-admission.md +15 -0
- package/docs/blocks/will-not-do.md +18 -0
- package/docs/coauthor-session.md +57 -0
- package/docs/connected-composition.md +75 -0
- package/docs/continuity.md +3 -4
- package/docs/decisions.md +167 -0
- package/docs/design.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 +99 -16
- 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/knowledge-graph.md +8 -4
- package/docs/local-review.md +84 -0
- package/docs/local-services.md +328 -0
- package/docs/local-state.md +107 -0
- package/docs/obsidian-contract.md +698 -0
- package/docs/obsidian.md +484 -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 +174 -12
- package/docs/repo-boundary-guard.md +12 -2
- package/docs/review-portability.md +55 -0
- package/docs/skill-steward.md +156 -0
- package/docs/upgrade.md +155 -5
- 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/projects/sample-workspace/content/source.html.kg.json +4 -1
- package/fixtures/projects/source-formats-workspace/content/data.json.kg.json +4 -1
- package/fixtures/projects/source-formats-workspace/content/logo.png.kg.json +4 -1
- package/fixtures/projects/source-formats-workspace/content/metrics.csv.kg.json +4 -1
- package/fixtures/projects/source-formats-workspace/content/pipeline.yaml.kg.json +4 -1
- package/fixtures/ui/presentation/pane.v1.json +7 -0
- package/fixtures/ui/presentation/reference.v1.json +293 -0
- package/package.json +86 -10
- package/skills/claude/atelier-guided-coauthor/SKILL.md +49 -0
- package/skills/claude/atelier-guided-upgrade/SKILL.md +97 -0
- package/skills/claude/atelier-local-service/SKILL.md +47 -0
- package/skills/claude/atelier-open/SKILL.md +11 -5
- package/skills/claude/atelier-public-boundary/SKILL.md +31 -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-local-service/SKILL.md +47 -0
- package/skills/codex/atelier-open/SKILL.md +11 -5
- package/skills/codex/atelier-public-boundary/SKILL.md +31 -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 +283 -20
- package/src/boundary/policy.mjs +213 -91
- package/src/cli/execute-command.mjs +36 -0
- package/src/cli/project-options.mjs +58 -0
- package/src/cli/run.mjs +78 -10
- package/src/coauthor/session.mjs +161 -0
- package/src/coauthor/store.mjs +173 -0
- package/src/collaboration/event-ledger.mjs +348 -0
- package/src/collaboration/index.mjs +17 -0
- package/src/collaboration/inspection-bundle.mjs +223 -0
- package/src/collaboration/proposals.mjs +265 -65
- package/src/collaboration/review-contracts.mjs +25 -0
- package/src/collaboration/review-store.mjs +305 -0
- package/src/commands/attestation.mjs +20 -6
- package/src/commands/coauthor.mjs +26 -0
- package/src/commands/config.mjs +6 -1
- package/src/commands/disclosure.mjs +133 -0
- package/src/commands/distribution.mjs +2 -1
- package/src/commands/extension-pack.mjs +17 -9
- package/src/commands/init.mjs +46 -6
- package/src/commands/obsidian.mjs +371 -0
- package/src/commands/review.mjs +147 -0
- package/src/commands/server.mjs +1 -4
- 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 +206 -0
- package/src/egress/check.mjs +7 -38
- package/src/egress/forbidden-egress.mjs +32 -18
- package/src/export/atelier-export-contract.mjs +16 -1
- package/src/extension-packs/lifecycle.mjs +154 -0
- package/src/graph/graph.mjs +176 -316
- package/src/graph/knowledge-graph.mjs +643 -43
- package/src/guides/contracts.mjs +54 -0
- package/src/harness/context-client.mjs +9 -1
- package/src/index.mjs +41 -0
- package/src/intake/store.mjs +119 -0
- package/src/preview/channel.mjs +89 -0
- package/src/project/config.mjs +112 -52
- package/src/project/durable-state.mjs +161 -0
- package/src/project/file-class.mjs +180 -0
- package/src/project/package-root.mjs +10 -0
- package/src/project/path-match.mjs +38 -15
- package/src/project/private-state.mjs +120 -0
- 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 +212 -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 +138 -0
- package/src/runtime/obsidian/app-production-seams.mjs +107 -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 +280 -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-entry-path.mjs +9 -0
- package/src/runtime/obsidian/service-main.mjs +93 -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 +125 -59
- package/src/server/security.mjs +89 -4
- package/src/server/server.mjs +4 -2
- package/src/skills/steward.mjs +780 -0
- package/src/support/feedback-report.mjs +4 -3
- 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 +65 -29
- 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,189 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process'
|
|
2
|
+
import fs from 'node:fs'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
|
|
5
|
+
const DEFAULT_TIMEOUT_MS = 30_000
|
|
6
|
+
const DEFAULT_MAX_BUFFER = 16 * 1024 * 1024
|
|
7
|
+
|
|
8
|
+
function executableNames(platform = process.platform, env = process.env) {
|
|
9
|
+
if (platform !== 'win32') return ['git']
|
|
10
|
+
const extensions = String(env.PATHEXT || '.EXE;.CMD;.BAT;.COM')
|
|
11
|
+
.split(';')
|
|
12
|
+
.map((value) => value.trim().toLowerCase())
|
|
13
|
+
.filter(Boolean)
|
|
14
|
+
return ['git.exe', ...extensions.map((extension) => `git${extension}`).filter((name) => name !== 'git.exe')]
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function isExecutable(file, platform = process.platform) {
|
|
18
|
+
try {
|
|
19
|
+
fs.accessSync(file, platform === 'win32' ? fs.constants.F_OK : fs.constants.X_OK)
|
|
20
|
+
return fs.statSync(file).isFile()
|
|
21
|
+
} catch {
|
|
22
|
+
return false
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function canonicalExecutable(file) {
|
|
27
|
+
try {
|
|
28
|
+
return fs.realpathSync.native(file)
|
|
29
|
+
} catch {
|
|
30
|
+
return fs.realpathSync(file)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Resolve one exact Git executable without invoking a shell. The selected path
|
|
36
|
+
* is carried through every later operation so PATH changes cannot swap engines
|
|
37
|
+
* underneath a running supervisor.
|
|
38
|
+
*/
|
|
39
|
+
export function resolveGitExecutable({ env = process.env, platform = process.platform } = {}) {
|
|
40
|
+
const configured = String(env.ATELIER_GIT_PATH || '').trim()
|
|
41
|
+
if (configured) {
|
|
42
|
+
if (!path.isAbsolute(configured)) throw new Error('ATELIER_GIT_PATH must be an absolute path')
|
|
43
|
+
if (!isExecutable(configured, platform)) throw new Error(`configured Git executable is unavailable: ${configured}`)
|
|
44
|
+
return canonicalExecutable(configured)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const searchPath = String(env.PATH || '')
|
|
48
|
+
const delimiter = platform === 'win32' ? ';' : path.delimiter
|
|
49
|
+
const names = executableNames(platform, env)
|
|
50
|
+
for (const directory of searchPath.split(delimiter).filter(Boolean)) {
|
|
51
|
+
const cleanDir = directory.replace(/^"|"$/g, '')
|
|
52
|
+
for (const name of names) {
|
|
53
|
+
const candidate = path.join(cleanDir, name)
|
|
54
|
+
if (isExecutable(candidate, platform)) return canonicalExecutable(candidate)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
throw new Error('compatible system Git was not found on PATH')
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function sanitizedGitEnvironment(env = process.env, allowPrompt = false) {
|
|
61
|
+
const next = { ...env }
|
|
62
|
+
for (const key of Object.keys(next)) {
|
|
63
|
+
if (key.toUpperCase().startsWith('GIT_')) delete next[key]
|
|
64
|
+
}
|
|
65
|
+
if (!allowPrompt) next.GIT_TERMINAL_PROMPT = '0'
|
|
66
|
+
next.GIT_OPTIONAL_LOCKS = next.GIT_OPTIONAL_LOCKS || '1'
|
|
67
|
+
next.GIT_NO_REPLACE_OBJECTS = '1'
|
|
68
|
+
return next
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function redactGitDiagnostic(value) {
|
|
72
|
+
return String(value || '')
|
|
73
|
+
.replace(/https?:\/\/[^\s]+/gi, (url) => sanitizeRemoteUrl(url))
|
|
74
|
+
.replace(/\b((?:password|passwd|token|secret|authorization|proxy-authorization|extraheader)\s*[=:]\s*)[^\s]+/gi, '$1[redacted]')
|
|
75
|
+
.replace(/\b(Bearer|Basic)\s+[A-Za-z0-9._~+/=-]+/gi, '$1 [redacted]')
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export class GitCommandError extends Error {
|
|
79
|
+
constructor(message, result) {
|
|
80
|
+
super(message)
|
|
81
|
+
this.name = 'GitCommandError'
|
|
82
|
+
this.result = result
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function runGit(gitExecutable, repoRoot, args, {
|
|
87
|
+
allowFailure = false,
|
|
88
|
+
allowPrompt = false,
|
|
89
|
+
env = process.env,
|
|
90
|
+
input = undefined,
|
|
91
|
+
timeout = DEFAULT_TIMEOUT_MS,
|
|
92
|
+
} = {}) {
|
|
93
|
+
if (!path.isAbsolute(gitExecutable)) throw new Error('Git executable must be an absolute path')
|
|
94
|
+
if (!Array.isArray(args) || args.some((value) => typeof value !== 'string')) {
|
|
95
|
+
throw new Error('Git arguments must be a string array')
|
|
96
|
+
}
|
|
97
|
+
const argv = repoRoot ? ['-C', repoRoot, ...args] : args
|
|
98
|
+
const child = spawnSync(gitExecutable, argv, {
|
|
99
|
+
encoding: 'utf8',
|
|
100
|
+
env: sanitizedGitEnvironment(env, allowPrompt),
|
|
101
|
+
input,
|
|
102
|
+
maxBuffer: DEFAULT_MAX_BUFFER,
|
|
103
|
+
shell: false,
|
|
104
|
+
timeout,
|
|
105
|
+
windowsHide: true,
|
|
106
|
+
})
|
|
107
|
+
const result = {
|
|
108
|
+
ok: !child.error && child.status === 0,
|
|
109
|
+
status: child.status,
|
|
110
|
+
signal: child.signal ?? null,
|
|
111
|
+
stdout: child.stdout || '',
|
|
112
|
+
stderr: redactGitDiagnostic(child.stderr || ''),
|
|
113
|
+
error: child.error?.message ? redactGitDiagnostic(child.error.message) : null,
|
|
114
|
+
executable: gitExecutable,
|
|
115
|
+
args: argv,
|
|
116
|
+
}
|
|
117
|
+
if (!result.ok && !allowFailure) {
|
|
118
|
+
const detail = result.error || result.stderr.trim() || `exit ${String(result.status)}`
|
|
119
|
+
throw new GitCommandError(`Git ${args[0] || 'command'} failed: ${detail}`, result)
|
|
120
|
+
}
|
|
121
|
+
return result
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function gitText(gitExecutable, repoRoot, args, options = {}) {
|
|
125
|
+
return runGit(gitExecutable, repoRoot, args, options).stdout.trim()
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function parseGitVersion(text) {
|
|
129
|
+
const match = String(text || '').match(/git version (\d+)\.(\d+)\.(\d+)/i)
|
|
130
|
+
if (!match) return null
|
|
131
|
+
return { major: Number(match[1]), minor: Number(match[2]), patch: Number(match[3]), text: match[0] }
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function isSupportedGitVersion(version) {
|
|
135
|
+
return Boolean(version && (version.major > 2 || (version.major === 2 && version.minor >= 40)))
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function inspectGitEngine(gitExecutable, { env = process.env } = {}) {
|
|
139
|
+
const result = runGit(gitExecutable, null, ['--version'], { env })
|
|
140
|
+
const version = parseGitVersion(result.stdout)
|
|
141
|
+
if (!version) throw new Error(`unrecognized Git version output from ${gitExecutable}`)
|
|
142
|
+
return {
|
|
143
|
+
executable: gitExecutable,
|
|
144
|
+
version: `${version.major}.${version.minor}.${version.patch}`,
|
|
145
|
+
supported: isSupportedGitVersion(version),
|
|
146
|
+
minimum: '2.40.0',
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function classifyRemoteAuthentication(url) {
|
|
151
|
+
const value = String(url || '').trim()
|
|
152
|
+
if (!value) return 'none'
|
|
153
|
+
if (/^(?:ssh:\/\/|[^/@\s]+@[^:/\s]+:)/i.test(value)) return 'ssh'
|
|
154
|
+
if (/^https?:\/\//i.test(value)) return 'https'
|
|
155
|
+
if (/^(?:file:\/\/|\.{0,2}[\\/]|[a-z]:[\\/]|[\\/])/i.test(value)) return 'local'
|
|
156
|
+
return 'unknown'
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Remote URLs are evidence, not authentication material. Git may carry HTTP
|
|
161
|
+
* credentials in user-info or query fragments, so local observations retain
|
|
162
|
+
* only the provider/repository address needed for diagnosis.
|
|
163
|
+
*/
|
|
164
|
+
export function sanitizeRemoteUrl(url) {
|
|
165
|
+
const value = String(url || '').trim()
|
|
166
|
+
const scpLike = value.match(/^[^/@\s]+@([^:/\s]+:.+)$/)
|
|
167
|
+
if (scpLike) return scpLike[1]
|
|
168
|
+
if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(value)) return value
|
|
169
|
+
try {
|
|
170
|
+
const parsed = new URL(value)
|
|
171
|
+
parsed.username = ''
|
|
172
|
+
Reflect.set(parsed, 'password', '')
|
|
173
|
+
parsed.search = ''
|
|
174
|
+
parsed.hash = ''
|
|
175
|
+
return parsed.toString()
|
|
176
|
+
} catch {
|
|
177
|
+
return value.replace(/^((?:https?|ssh):\/\/)(?:[^/@:]+:)?[^/@]+@/i, '$1')
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function parseNullConfig(text) {
|
|
182
|
+
const entries = []
|
|
183
|
+
for (const record of String(text || '').split('\0').filter(Boolean)) {
|
|
184
|
+
const newline = record.indexOf('\n')
|
|
185
|
+
if (newline === -1) entries.push({ key: record, value: '' })
|
|
186
|
+
else entries.push({ key: record.slice(0, newline), value: record.slice(newline + 1) })
|
|
187
|
+
}
|
|
188
|
+
return entries
|
|
189
|
+
}
|
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
import crypto from 'node:crypto'
|
|
2
|
+
import fs from 'node:fs'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import {
|
|
5
|
+
atomicReplacePrivateText,
|
|
6
|
+
ensureContainedPrivateDirectory,
|
|
7
|
+
openRegularFileNoFollow,
|
|
8
|
+
readRegularTextNoFollow,
|
|
9
|
+
} from '../project/private-state.mjs'
|
|
10
|
+
import { runGit } from './git-adapter.mjs'
|
|
11
|
+
|
|
12
|
+
export const ATELIER_RUNTIME_STATE_SCHEMA = 'atelier-runtime-state@v1'
|
|
13
|
+
export const ATELIER_RUNTIME_ENROLLMENT_SCHEMA = 'atelier-runtime-enrollment@v1'
|
|
14
|
+
export const ATELIER_RUNTIME_TRACE_SCHEMA = 'atelier-runtime-operation@v1'
|
|
15
|
+
export const ATELIER_RUNTIME_TRACE_MAX_BYTES = 2 * 1024 * 1024
|
|
16
|
+
export const ATELIER_RUNTIME_TRACE_MAX_RECORDS = 2048
|
|
17
|
+
export const ATELIER_RUNTIME_TRACE_MAX_EVENT_BYTES = 256 * 1024
|
|
18
|
+
export const ATELIER_RUNTIME_PLAN_MAX_AGE_MS = 24 * 60 * 60 * 1000
|
|
19
|
+
export const ATELIER_RUNTIME_PLAN_MAX_FILES = 256
|
|
20
|
+
export const ATELIER_RUNTIME_PLAN_MAX_BYTES = 4 * 1024 * 1024
|
|
21
|
+
export const ATELIER_RUNTIME_STATE_MAX_BYTES = 512 * 1024
|
|
22
|
+
export const ATELIER_RUNTIME_LIVE_OWNER_MAX_AGE_MS = 24 * 60 * 60 * 1000
|
|
23
|
+
|
|
24
|
+
function lstatIfPresent(file) {
|
|
25
|
+
try {
|
|
26
|
+
return fs.lstatSync(file)
|
|
27
|
+
} catch (error) {
|
|
28
|
+
if (error?.code === 'ENOENT') return null
|
|
29
|
+
throw error
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function runtimeWorkspaceRoot(file) {
|
|
34
|
+
const absolute = path.resolve(file)
|
|
35
|
+
const marker = `${path.sep}.atelier-local${path.sep}`
|
|
36
|
+
const index = absolute.lastIndexOf(marker)
|
|
37
|
+
if (index <= 0) throw new Error('runtime state path is outside .atelier-local')
|
|
38
|
+
return absolute.slice(0, index)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function secureRuntimeLeaf(file) {
|
|
42
|
+
const workspaceRoot = runtimeWorkspaceRoot(file)
|
|
43
|
+
const directory = ensureContainedPrivateDirectory({
|
|
44
|
+
workspaceRoot,
|
|
45
|
+
directory: path.dirname(path.resolve(file)),
|
|
46
|
+
label: 'runtime state directory',
|
|
47
|
+
})
|
|
48
|
+
return path.join(directory, path.basename(file))
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function atomicWriteJson(file, value) {
|
|
52
|
+
const secured = secureRuntimeLeaf(file)
|
|
53
|
+
atomicReplacePrivateText(secured, `${JSON.stringify(value, null, 2)}\n`)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function readJsonIfPresent(file) {
|
|
57
|
+
const secured = secureRuntimeLeaf(file)
|
|
58
|
+
const stat = lstatIfPresent(secured)
|
|
59
|
+
if (!stat) return null
|
|
60
|
+
if (stat.size > ATELIER_RUNTIME_PLAN_MAX_BYTES) throw new Error('runtime JSON exceeds the resident byte ceiling')
|
|
61
|
+
return JSON.parse(readRegularTextNoFollow(secured))
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function removeRuntimeLeaf(file) {
|
|
65
|
+
const secured = secureRuntimeLeaf(file)
|
|
66
|
+
const stat = lstatIfPresent(secured)
|
|
67
|
+
if (!stat) return false
|
|
68
|
+
if (stat.isSymbolicLink() || !stat.isFile()) throw new Error('state leaf is not a regular file')
|
|
69
|
+
fs.unlinkSync(secured)
|
|
70
|
+
return true
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function runtimePlanInventory(plansPath) {
|
|
74
|
+
const directory = ensureContainedPrivateDirectory({
|
|
75
|
+
workspaceRoot: runtimeWorkspaceRoot(plansPath),
|
|
76
|
+
directory: plansPath,
|
|
77
|
+
label: 'runtime plans directory',
|
|
78
|
+
})
|
|
79
|
+
const files = []
|
|
80
|
+
let bytes = 0
|
|
81
|
+
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
|
|
82
|
+
if (!entry.name.endsWith('.json')) continue
|
|
83
|
+
if (entry.isSymbolicLink() || !entry.isFile()) throw new Error('runtime plans directory contains a redirected or non-file entry')
|
|
84
|
+
const file = path.join(directory, entry.name)
|
|
85
|
+
const stat = fs.lstatSync(file)
|
|
86
|
+
files.push({ file, name: entry.name, bytes: stat.size, mtimeMs: stat.mtimeMs })
|
|
87
|
+
bytes += stat.size
|
|
88
|
+
}
|
|
89
|
+
return { files, bytes }
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function runtimePaths(repoRoot) {
|
|
93
|
+
const root = path.join(repoRoot, '.atelier-local', 'runtime')
|
|
94
|
+
return {
|
|
95
|
+
root,
|
|
96
|
+
enrollment: path.join(root, 'enrollment.json'),
|
|
97
|
+
state: path.join(root, 'state.json'),
|
|
98
|
+
control: path.join(root, 'control.json'),
|
|
99
|
+
trace: path.join(root, 'operations.ndjson'),
|
|
100
|
+
plans: path.join(root, 'plans'),
|
|
101
|
+
lock: path.join(root, 'operation.lock'),
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function ensureRuntimeStateRoot(repoRoot, gitExecutable) {
|
|
106
|
+
const probe = '.atelier-local/runtime/.ignore-probe'
|
|
107
|
+
const ignored = runGit(gitExecutable, repoRoot, ['check-ignore', '-q', probe], { allowFailure: true })
|
|
108
|
+
if (!ignored.ok) {
|
|
109
|
+
throw new Error('.atelier-local/ must be Git-ignored before Atelier Sync writes machine-local state')
|
|
110
|
+
}
|
|
111
|
+
const root = ensureContainedPrivateDirectory({
|
|
112
|
+
workspaceRoot: repoRoot,
|
|
113
|
+
directory: path.join(repoRoot, '.atelier-local', 'runtime'),
|
|
114
|
+
label: 'runtime state directory',
|
|
115
|
+
})
|
|
116
|
+
const plans = ensureContainedPrivateDirectory({
|
|
117
|
+
workspaceRoot: repoRoot,
|
|
118
|
+
directory: path.join(root, 'plans'),
|
|
119
|
+
label: 'runtime plans directory',
|
|
120
|
+
})
|
|
121
|
+
return { ...runtimePaths(repoRoot), root, plans, lock: path.join(root, 'operation.lock') }
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function stableJson(value) {
|
|
125
|
+
if (Array.isArray(value)) return `[${value.map(stableJson).join(',')}]`
|
|
126
|
+
if (!value || typeof value !== 'object') return JSON.stringify(value)
|
|
127
|
+
return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`).join(',')}}`
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function digest(value) {
|
|
131
|
+
return crypto.createHash('sha256').update(stableJson(value)).digest('hex')
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function readOperationTrace(tracePath) {
|
|
135
|
+
const secured = secureRuntimeLeaf(tracePath)
|
|
136
|
+
const stat = lstatIfPresent(secured)
|
|
137
|
+
if (!stat) return []
|
|
138
|
+
if (stat.size > ATELIER_RUNTIME_TRACE_MAX_BYTES) throw new Error('runtime trace exceeds the resident byte ceiling')
|
|
139
|
+
const records = readRegularTextNoFollow(secured).split('\n').filter(Boolean).map((line, index) => {
|
|
140
|
+
try {
|
|
141
|
+
return JSON.parse(line)
|
|
142
|
+
} catch (error) {
|
|
143
|
+
throw new Error(`invalid runtime trace line ${index + 1}: ${error.message}`)
|
|
144
|
+
}
|
|
145
|
+
})
|
|
146
|
+
if (records.length > ATELIER_RUNTIME_TRACE_MAX_RECORDS) throw new Error('runtime trace exceeds the resident record ceiling')
|
|
147
|
+
let previousHash = null
|
|
148
|
+
records.forEach((record, index) => {
|
|
149
|
+
const { hash, ...unsigned } = record
|
|
150
|
+
if (record.sequence !== index + 1) throw new Error(`runtime trace sequence gap at line ${index + 1}`)
|
|
151
|
+
if ((record.previousHash ?? null) !== previousHash) throw new Error(`runtime trace chain mismatch at line ${index + 1}`)
|
|
152
|
+
if (digest(unsigned) !== hash) throw new Error(`runtime trace hash mismatch at line ${index + 1}`)
|
|
153
|
+
previousHash = hash
|
|
154
|
+
})
|
|
155
|
+
return records
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function boundedTraceEvent(event) {
|
|
159
|
+
const encoded = JSON.stringify(event)
|
|
160
|
+
const originalBytes = Buffer.byteLength(encoded)
|
|
161
|
+
if (originalBytes <= ATELIER_RUNTIME_TRACE_MAX_EVENT_BYTES) return event
|
|
162
|
+
return {
|
|
163
|
+
at: event?.at,
|
|
164
|
+
operation: event?.operation || 'trace-event',
|
|
165
|
+
outcome: event?.outcome || 'recorded',
|
|
166
|
+
...(event?.operationId ? { operationId: String(event.operationId) } : {}),
|
|
167
|
+
...(event?.mode ? { mode: String(event.mode) } : {}),
|
|
168
|
+
details: {
|
|
169
|
+
truncated: true,
|
|
170
|
+
originalBytes,
|
|
171
|
+
originalSha256: crypto.createHash('sha256').update(encoded).digest('hex'),
|
|
172
|
+
},
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function traceRecoveryCheckpoint(secured, event, reason) {
|
|
177
|
+
const stat = lstatIfPresent(secured)
|
|
178
|
+
let priorSha256 = null
|
|
179
|
+
try {
|
|
180
|
+
if ((stat?.size ?? 0) <= ATELIER_RUNTIME_TRACE_MAX_BYTES) {
|
|
181
|
+
priorSha256 = crypto.createHash('sha256').update(readRegularTextNoFollow(secured)).digest('hex')
|
|
182
|
+
}
|
|
183
|
+
} catch {
|
|
184
|
+
// The recovery checkpoint still records the size and reason if the old
|
|
185
|
+
// regular file cannot be read completely.
|
|
186
|
+
}
|
|
187
|
+
const checkpointUnsigned = {
|
|
188
|
+
schema: ATELIER_RUNTIME_TRACE_SCHEMA,
|
|
189
|
+
sequence: 1,
|
|
190
|
+
previousHash: null,
|
|
191
|
+
at: event.at,
|
|
192
|
+
operation: 'trace-checkpoint',
|
|
193
|
+
outcome: 'recovered',
|
|
194
|
+
details: { reason, priorBytes: stat?.size ?? 0, priorSha256 },
|
|
195
|
+
}
|
|
196
|
+
const checkpoint = { ...checkpointUnsigned, hash: digest(checkpointUnsigned) }
|
|
197
|
+
atomicReplacePrivateText(secured, `${JSON.stringify(checkpoint)}\n`)
|
|
198
|
+
return checkpoint
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function appendOperationTrace(tracePath, event) {
|
|
202
|
+
const secured = secureRuntimeLeaf(tracePath)
|
|
203
|
+
const boundedEvent = boundedTraceEvent(event)
|
|
204
|
+
let records
|
|
205
|
+
try {
|
|
206
|
+
records = readOperationTrace(secured)
|
|
207
|
+
} catch (error) {
|
|
208
|
+
records = [traceRecoveryCheckpoint(secured, boundedEvent, error.message)]
|
|
209
|
+
}
|
|
210
|
+
let previousHash = records.at(-1)?.hash ?? null
|
|
211
|
+
const nextProbe = JSON.stringify({ schema: ATELIER_RUNTIME_TRACE_SCHEMA, sequence: records.length + 1, previousHash, ...boundedEvent, hash: '0'.repeat(64) })
|
|
212
|
+
const currentBytes = lstatIfPresent(secured)?.size ?? 0
|
|
213
|
+
if (records.length >= ATELIER_RUNTIME_TRACE_MAX_RECORDS || currentBytes + Buffer.byteLength(`${nextProbe}\n`) > ATELIER_RUNTIME_TRACE_MAX_BYTES) {
|
|
214
|
+
const checkpointUnsigned = {
|
|
215
|
+
schema: ATELIER_RUNTIME_TRACE_SCHEMA,
|
|
216
|
+
sequence: 1,
|
|
217
|
+
previousHash: null,
|
|
218
|
+
at: boundedEvent.at,
|
|
219
|
+
operation: 'trace-checkpoint',
|
|
220
|
+
outcome: 'compacted',
|
|
221
|
+
details: { compactedRecords: records.length, priorLastHash: previousHash },
|
|
222
|
+
}
|
|
223
|
+
const checkpoint = { ...checkpointUnsigned, hash: digest(checkpointUnsigned) }
|
|
224
|
+
atomicReplacePrivateText(secured, `${JSON.stringify(checkpoint)}\n`)
|
|
225
|
+
records = [checkpoint]
|
|
226
|
+
previousHash = checkpoint.hash
|
|
227
|
+
}
|
|
228
|
+
const unsigned = {
|
|
229
|
+
schema: ATELIER_RUNTIME_TRACE_SCHEMA,
|
|
230
|
+
sequence: records.length + 1,
|
|
231
|
+
previousHash,
|
|
232
|
+
...boundedEvent,
|
|
233
|
+
}
|
|
234
|
+
const record = { ...unsigned, hash: digest(unsigned) }
|
|
235
|
+
const descriptor = openRegularFileNoFollow(
|
|
236
|
+
secured,
|
|
237
|
+
fs.constants.O_WRONLY | fs.constants.O_APPEND | fs.constants.O_CREAT,
|
|
238
|
+
0o600,
|
|
239
|
+
)
|
|
240
|
+
try {
|
|
241
|
+
fs.writeFileSync(descriptor, `${JSON.stringify(record)}\n`)
|
|
242
|
+
fs.fsyncSync(descriptor)
|
|
243
|
+
} finally {
|
|
244
|
+
fs.closeSync(descriptor)
|
|
245
|
+
}
|
|
246
|
+
try {
|
|
247
|
+
fs.chmodSync(secured, 0o600)
|
|
248
|
+
} catch {
|
|
249
|
+
// Windows inherits the current user's ACL; POSIX mode is best effort.
|
|
250
|
+
}
|
|
251
|
+
return record
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function processAlive(pid) {
|
|
255
|
+
if (!Number.isInteger(pid) || pid <= 0) return false
|
|
256
|
+
try {
|
|
257
|
+
process.kill(pid, 0)
|
|
258
|
+
return true
|
|
259
|
+
} catch (error) {
|
|
260
|
+
return error?.code === 'EPERM'
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function removeOwnedLock(lockPath, lockNonce) {
|
|
265
|
+
const stat = lstatIfPresent(lockPath)
|
|
266
|
+
if (!stat) return
|
|
267
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) throw new Error('runtime lock is redirected or not a directory')
|
|
268
|
+
const ownerFile = path.join(lockPath, 'owner.json')
|
|
269
|
+
const owner = readJsonIfPresent(ownerFile)
|
|
270
|
+
if (!lockNonce || owner?.lockNonce !== lockNonce) throw new Error('runtime lock ownership changed before release')
|
|
271
|
+
const quarantine = `${lockPath}.release-${lockNonce}`
|
|
272
|
+
fs.renameSync(lockPath, quarantine)
|
|
273
|
+
fs.rmSync(quarantine, { recursive: true, force: true })
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export function acquireRepositoryLock(paths, { operation, clock = () => new Date().toISOString(), ownerWriteGraceMs = 30_000 } = {}) {
|
|
277
|
+
ensureContainedPrivateDirectory({ workspaceRoot: runtimeWorkspaceRoot(paths.root), directory: paths.root, label: 'runtime state directory' })
|
|
278
|
+
const lockNonce = crypto.randomBytes(16).toString('hex')
|
|
279
|
+
try {
|
|
280
|
+
fs.mkdirSync(paths.lock, { mode: 0o700 })
|
|
281
|
+
} catch (error) {
|
|
282
|
+
if (error.code !== 'EEXIST') throw error
|
|
283
|
+
const lockStat = lstatIfPresent(paths.lock)
|
|
284
|
+
if (!lockStat || lockStat.isSymbolicLink() || !lockStat.isDirectory()) {
|
|
285
|
+
throw new Error('runtime lock is redirected or not a directory')
|
|
286
|
+
}
|
|
287
|
+
let owner = null
|
|
288
|
+
try {
|
|
289
|
+
owner = readJsonIfPresent(path.join(paths.lock, 'owner.json'))
|
|
290
|
+
} catch (ownerError) {
|
|
291
|
+
if (/redirected|regular file|escapes workspace/.test(ownerError.message)) throw ownerError
|
|
292
|
+
// A truncated owner file is not authority to delete a possibly live lock.
|
|
293
|
+
}
|
|
294
|
+
const ownerTimestamp = Date.parse(owner?.acquiredAt || '')
|
|
295
|
+
const ownerAgeMs = Date.now() - (Number.isFinite(ownerTimestamp) ? ownerTimestamp : lockStat.mtimeMs)
|
|
296
|
+
if (owner && processAlive(owner.pid) && ownerAgeMs < ATELIER_RUNTIME_LIVE_OWNER_MAX_AGE_MS) {
|
|
297
|
+
return { ok: false, code: 'repository-busy', owner }
|
|
298
|
+
}
|
|
299
|
+
const currentLockStat = lstatIfPresent(paths.lock)
|
|
300
|
+
if (!currentLockStat || currentLockStat.isSymbolicLink() || !currentLockStat.isDirectory()) {
|
|
301
|
+
throw new Error('runtime lock is redirected or not a directory')
|
|
302
|
+
}
|
|
303
|
+
const ageMs = Date.now() - currentLockStat.mtimeMs
|
|
304
|
+
if (ageMs < ownerWriteGraceMs) return { ok: false, code: 'repository-busy', owner: owner || { operation: 'lock-owner-pending', ageMs } }
|
|
305
|
+
const claimPath = path.join(paths.lock, 'recovery.claim')
|
|
306
|
+
let claimDescriptor
|
|
307
|
+
try {
|
|
308
|
+
claimDescriptor = openRegularFileNoFollow(
|
|
309
|
+
claimPath,
|
|
310
|
+
fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL,
|
|
311
|
+
0o600,
|
|
312
|
+
)
|
|
313
|
+
fs.writeFileSync(claimDescriptor, lockNonce)
|
|
314
|
+
fs.fsyncSync(claimDescriptor)
|
|
315
|
+
} catch (claimError) {
|
|
316
|
+
if (claimError?.code === 'EEXIST') {
|
|
317
|
+
const claimStat = lstatIfPresent(claimPath)
|
|
318
|
+
if (!claimStat || claimStat.isSymbolicLink() || !claimStat.isFile()) throw new Error('runtime recovery claim is redirected or not a regular file')
|
|
319
|
+
const claimAgeMs = Date.now() - claimStat.mtimeMs
|
|
320
|
+
if (claimAgeMs < ownerWriteGraceMs) {
|
|
321
|
+
return { ok: false, code: 'repository-busy', owner: owner || { operation: 'stale-lock-recovery', ageMs: claimAgeMs } }
|
|
322
|
+
}
|
|
323
|
+
const staleClaim = `${claimPath}.stale-${lockNonce}`
|
|
324
|
+
try {
|
|
325
|
+
fs.renameSync(claimPath, staleClaim)
|
|
326
|
+
const movedClaimStat = lstatIfPresent(staleClaim)
|
|
327
|
+
if (!movedClaimStat || movedClaimStat.dev !== claimStat.dev || movedClaimStat.ino !== claimStat.ino) {
|
|
328
|
+
if (movedClaimStat && !lstatIfPresent(claimPath)) fs.renameSync(staleClaim, claimPath)
|
|
329
|
+
return { ok: false, code: 'repository-busy', owner: owner || { operation: 'stale-lock-recovery-identity-changed' } }
|
|
330
|
+
}
|
|
331
|
+
fs.unlinkSync(staleClaim)
|
|
332
|
+
claimDescriptor = openRegularFileNoFollow(
|
|
333
|
+
claimPath,
|
|
334
|
+
fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL,
|
|
335
|
+
0o600,
|
|
336
|
+
)
|
|
337
|
+
fs.writeFileSync(claimDescriptor, lockNonce)
|
|
338
|
+
fs.fsyncSync(claimDescriptor)
|
|
339
|
+
} catch (retryError) {
|
|
340
|
+
if (retryError?.code === 'EEXIST' || retryError?.code === 'ENOENT') {
|
|
341
|
+
return { ok: false, code: 'repository-busy', owner: owner || { operation: 'stale-lock-recovery-lost' } }
|
|
342
|
+
}
|
|
343
|
+
throw retryError
|
|
344
|
+
}
|
|
345
|
+
} else if (claimError?.code === 'ENOENT') {
|
|
346
|
+
return { ok: false, code: 'repository-busy', owner: owner || { operation: 'stale-lock-recovery' } }
|
|
347
|
+
} else {
|
|
348
|
+
throw claimError
|
|
349
|
+
}
|
|
350
|
+
} finally {
|
|
351
|
+
if (claimDescriptor != null) fs.closeSync(claimDescriptor)
|
|
352
|
+
}
|
|
353
|
+
let currentOwner = null
|
|
354
|
+
try {
|
|
355
|
+
currentOwner = readJsonIfPresent(path.join(paths.lock, 'owner.json'))
|
|
356
|
+
} catch (ownerError) {
|
|
357
|
+
if (/redirected|regular file|escapes workspace/.test(ownerError.message)) throw ownerError
|
|
358
|
+
// The exclusive recovery claim and age gate permit quarantining a stale,
|
|
359
|
+
// corrupt owner record; corruption is never authority to touch a new lock.
|
|
360
|
+
}
|
|
361
|
+
const currentOwnerTimestamp = Date.parse(currentOwner?.acquiredAt || '')
|
|
362
|
+
const currentOwnerAgeMs = Date.now() - (Number.isFinite(currentOwnerTimestamp) ? currentOwnerTimestamp : lockStat.mtimeMs)
|
|
363
|
+
if (currentOwner && processAlive(currentOwner.pid) && currentOwnerAgeMs < ATELIER_RUNTIME_LIVE_OWNER_MAX_AGE_MS) {
|
|
364
|
+
try {
|
|
365
|
+
if (readRegularTextNoFollow(claimPath) === lockNonce) fs.unlinkSync(claimPath)
|
|
366
|
+
} catch {
|
|
367
|
+
// The current owner controls the lock; leave it untouched on uncertainty.
|
|
368
|
+
}
|
|
369
|
+
return { ok: false, code: 'repository-busy', owner: currentOwner }
|
|
370
|
+
}
|
|
371
|
+
const quarantine = `${paths.lock}.stale-${lockNonce}`
|
|
372
|
+
try {
|
|
373
|
+
const beforeRecovery = lstatIfPresent(paths.lock)
|
|
374
|
+
if (!beforeRecovery || beforeRecovery.dev !== lockStat.dev || beforeRecovery.ino !== lockStat.ino) {
|
|
375
|
+
return { ok: false, code: 'repository-busy', owner: { operation: 'stale-lock-recovery-identity-changed' } }
|
|
376
|
+
}
|
|
377
|
+
fs.renameSync(paths.lock, quarantine)
|
|
378
|
+
const movedLock = lstatIfPresent(quarantine)
|
|
379
|
+
if (!movedLock || movedLock.dev !== lockStat.dev || movedLock.ino !== lockStat.ino) {
|
|
380
|
+
if (movedLock && !lstatIfPresent(paths.lock)) fs.renameSync(quarantine, paths.lock)
|
|
381
|
+
return { ok: false, code: 'repository-busy', owner: { operation: 'stale-lock-recovery-identity-changed' } }
|
|
382
|
+
}
|
|
383
|
+
fs.mkdirSync(paths.lock, { mode: 0o700 })
|
|
384
|
+
} catch (recoveryError) {
|
|
385
|
+
if (recoveryError?.code === 'EEXIST' || recoveryError?.code === 'ENOENT') {
|
|
386
|
+
if (lstatIfPresent(quarantine)) fs.rmSync(quarantine, { recursive: true, force: true })
|
|
387
|
+
return { ok: false, code: 'repository-busy', owner: { operation: 'stale-lock-recovery-lost' } }
|
|
388
|
+
}
|
|
389
|
+
throw recoveryError
|
|
390
|
+
}
|
|
391
|
+
fs.rmSync(quarantine, { recursive: true, force: true })
|
|
392
|
+
}
|
|
393
|
+
const owner = { lockNonce, pid: process.pid, operation, acquiredAt: clock() }
|
|
394
|
+
atomicWriteJson(path.join(paths.lock, 'owner.json'), owner)
|
|
395
|
+
return {
|
|
396
|
+
ok: true,
|
|
397
|
+
owner,
|
|
398
|
+
release() {
|
|
399
|
+
removeOwnedLock(paths.lock, lockNonce)
|
|
400
|
+
},
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
export function writeRuntimeState(paths, state) {
|
|
405
|
+
let value = { schema: ATELIER_RUNTIME_STATE_SCHEMA, ...state }
|
|
406
|
+
const encodedBytes = Buffer.byteLength(`${JSON.stringify(value, null, 2)}\n`)
|
|
407
|
+
if (encodedBytes > ATELIER_RUNTIME_STATE_MAX_BYTES) {
|
|
408
|
+
value = {
|
|
409
|
+
schema: ATELIER_RUNTIME_STATE_SCHEMA,
|
|
410
|
+
status: 'attention',
|
|
411
|
+
code: 'runtime-state-resident-ceiling',
|
|
412
|
+
message: 'runtime state exceeded its resident byte ceiling and was compacted',
|
|
413
|
+
incidentId: state.incidentId ?? null,
|
|
414
|
+
updatedAt: state.updatedAt ?? new Date().toISOString(),
|
|
415
|
+
observation: state.observation ? {
|
|
416
|
+
sourceSchema: state.observation.sourceSchema ?? state.observation.schema,
|
|
417
|
+
observedAt: state.observation.observedAt,
|
|
418
|
+
complete: false,
|
|
419
|
+
root: state.observation.root,
|
|
420
|
+
branch: state.observation.branch ? { branch: state.observation.branch.branch, head: state.observation.branch.head } : null,
|
|
421
|
+
status: state.observation.status ? { clean: state.observation.status.clean, digest: state.observation.status.digest } : null,
|
|
422
|
+
blockers: [{ code: 'runtime-state-resident-ceiling', message: 'full resident state was too large to retain', details: { limitBytes: ATELIER_RUNTIME_STATE_MAX_BYTES, observedBytes: encodedBytes } }],
|
|
423
|
+
} : null,
|
|
424
|
+
details: { limitBytes: ATELIER_RUNTIME_STATE_MAX_BYTES, observedBytes: encodedBytes },
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
atomicWriteJson(paths.state, value)
|
|
428
|
+
return value
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
export function readRuntimeControl(paths) {
|
|
432
|
+
return readJsonIfPresent(paths.control) || { paused: false, reason: null, updatedAt: null }
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
export function writeRuntimeControl(paths, { paused, reason = null, updatedAt = new Date().toISOString() }) {
|
|
436
|
+
const control = { paused: Boolean(paused), reason: paused ? String(reason || 'paused by user') : null, updatedAt }
|
|
437
|
+
atomicWriteJson(paths.control, control)
|
|
438
|
+
return control
|
|
439
|
+
}
|