@mnstry/atelier 0.2.0-alpha.5 → 0.2.0-alpha.6

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/docs/upgrade.md CHANGED
@@ -5,6 +5,21 @@ workspace between Atelier package releases. The flow is local-only: it does not
5
5
  provision repositories, contact a Git host, mutate the MNSTRY runtime, or write
6
6
  through a browser view.
7
7
 
8
+ ## Upgrading to 0.2.0-alpha.6
9
+
10
+ This release adds Atelier Sync Deliverable Zero: a headless, local repository
11
+ supervisor with explicit enrollment, complete repository observations,
12
+ fast-forward-only reconciliation, pause/resume control, hash-chained local
13
+ operation traces, and two-phase user-confirmed commit and publication. It does
14
+ not add a desktop shell, semantic conflict resolution, force operations,
15
+ telemetry, or unattended semantic writes.
16
+
17
+ Consumers that adopt the new runtime should enroll one repository explicitly,
18
+ treat watchers as hints rather than truth, require a fresh complete observation
19
+ before each confirmed mutation, and keep commit and publication as separate
20
+ user decisions. Existing consumers that do not import the runtime subpaths are
21
+ contract-compatible and need only update their pinned dependency.
22
+
8
23
  ## Upgrading to 0.2.0-alpha.5
9
24
 
10
25
  This release hardens public/private boundary enforcement, local serving,
@@ -117,7 +132,7 @@ For registry installs, pin the exact version and record the resolved version
117
132
  in the lockfile:
118
133
 
119
134
  ```bash
120
- npm install --save-dev @mnstry/atelier@0.2.0-alpha.5
135
+ npm install --save-dev @mnstry/atelier@0.2.0-alpha.6
121
136
  npx mnstry-atelier lock write --project ./atelier.project.json
122
137
  ```
123
138
 
@@ -125,7 +140,7 @@ For Git installs, pin the release tag rather than a branch, so the lock file
125
140
  records exactly what was reviewed:
126
141
 
127
142
  ```bash
128
- npm install --save-dev "git+https://github.com/MNSTRY/atelier.git#v0.2.0-alpha.5"
143
+ npm install --save-dev "git+https://github.com/MNSTRY/atelier.git#v0.2.0-alpha.6"
129
144
  npx mnstry-atelier lock write --project ./atelier.project.json
130
145
  ```
131
146
 
@@ -0,0 +1,18 @@
1
+ {
2
+ "schema": "atelier-repository-observation@v1",
3
+ "observedAt": "2026-08-25T00:00:00.000Z",
4
+ "complete": true,
5
+ "root": "/workspace/project",
6
+ "filesystem": {
7
+ "supported": true,
8
+ "code": "local-filesystem"
9
+ },
10
+ "blockers": [
11
+ {
12
+ "code": "partial-clone-unsupported",
13
+ "message": "partial clones may omit required Git objects",
14
+ "details": {}
15
+ }
16
+ ],
17
+ "warnings": []
18
+ }
@@ -0,0 +1,48 @@
1
+ {
2
+ "schema": "atelier-repository-observation@v1",
3
+ "observedAt": "2026-08-25T00:00:00.000Z",
4
+ "complete": true,
5
+ "root": "/workspace/project",
6
+ "filesystem": {
7
+ "supported": true,
8
+ "code": "local-filesystem"
9
+ },
10
+ "git": {
11
+ "executable": "/usr/bin/git",
12
+ "version": "2.40.0",
13
+ "supported": true,
14
+ "minimum": "2.40.0"
15
+ },
16
+ "bare": false,
17
+ "branch": {
18
+ "branch": "main",
19
+ "detached": false,
20
+ "head": "1111111111111111111111111111111111111111",
21
+ "upstream": "origin/main",
22
+ "ahead": 0,
23
+ "behind": 0
24
+ },
25
+ "remotes": [],
26
+ "features": {
27
+ "sparseCheckout": false,
28
+ "partialClone": [],
29
+ "customFilters": []
30
+ },
31
+ "submodules": {
32
+ "complete": true
33
+ },
34
+ "lfs": {
35
+ "complete": true
36
+ },
37
+ "status": {
38
+ "clean": true,
39
+ "digest": "2222222222222222222222222222222222222222222222222222222222222222",
40
+ "entries": [],
41
+ "fingerprints": [],
42
+ "stagedCount": 0,
43
+ "unstagedCount": 0,
44
+ "conflictCount": 0
45
+ },
46
+ "blockers": [],
47
+ "warnings": []
48
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mnstry/atelier",
3
- "version": "0.2.0-alpha.5",
3
+ "version": "0.2.0-alpha.6",
4
4
  "private": false,
5
5
  "description": "Local toolkit that turns a Git repository into an ontology-governed knowledge graph, enforcement layer, and runtime for people, agents, and tools. No telemetry, service, or account.",
6
6
  "keywords": [
@@ -41,6 +41,9 @@
41
41
  "./attestation": "./src/attestation/sign.mjs",
42
42
  "./extension-packs": "./src/extension-packs/loader.mjs",
43
43
  "./analysis/adapter": "./src/analysis/adapter.mjs",
44
+ "./runtime": "./src/runtime/supervisor.mjs",
45
+ "./runtime/git": "./src/runtime/git-adapter.mjs",
46
+ "./runtime/observation": "./src/runtime/repository-observation.mjs",
44
47
  "./export-contract": "./src/export/atelier-export-contract.mjs",
45
48
  "./dry-run": "./src/validate-atelier-export-dry-run.mjs",
46
49
  "./contracts/atelier-export.v1.schema.json": "./contracts/atelier-export.v1.schema.json",
@@ -60,6 +63,7 @@
60
63
  "readiness:test": "node --test test/readiness.test.mjs test/readiness-summary.test.mjs test/readiness-protocol-contract.test.mjs test/readiness-protocols.test.mjs test/readiness-protocol-runtime.test.mjs",
61
64
  "harness:test": "node --test test/harness.test.mjs test/harness-context.test.mjs",
62
65
  "collaboration:test": "node --test test/collaboration-ledger.test.mjs test/server-proposals.test.mjs",
66
+ "sync:test": "node --test test/runtime-git-adapter.test.mjs test/runtime-observation.test.mjs test/runtime-supervisor.test.mjs",
63
67
  "boundary:test": "node --test test/boundary-policy-contract.test.mjs test/boundary-content-rules.test.mjs test/boundary-guard.test.mjs",
64
68
  "upgrade:test": "node --test test/upgrade-contract.test.mjs test/upgrade.test.mjs",
65
69
  "release:audit": "node scripts/check-release-tarball.mjs",
@@ -2,6 +2,7 @@ import { spawnSync } from 'node:child_process'
2
2
  import fs from 'node:fs'
3
3
  import path from 'node:path'
4
4
  import { matchesPathPattern, normalizeRelPath } from '../project/path-match.mjs'
5
+ import { sanitizedGitEnvironment } from '../runtime/git-adapter.mjs'
5
6
 
6
7
  export const EMPTY_TREE = '4b825dc642cb6eb9a060e54bf8d69288fbee4904'
7
8
  export const ZERO_SHA_RE = /^0{40,}$/
@@ -252,12 +253,13 @@ export function parsePushRefInput(text) {
252
253
 
253
254
  export function gitOutputResult(repoRoot, args, {
254
255
  encoding = 'utf8',
256
+ gitExecutable = 'git',
255
257
  maxBuffer = DIFF_RESULT_MAX_BYTES,
256
258
  runner = spawnSync,
257
259
  } = {}) {
258
260
  let result
259
261
  try {
260
- result = runner('git', ['-C', repoRoot, ...args], { encoding, maxBuffer })
262
+ result = runner(gitExecutable, ['-C', repoRoot, ...args], { encoding, maxBuffer, env: sanitizedGitEnvironment() })
261
263
  } catch (error) {
262
264
  return { ok: false, code: 'git-command-failed', error: error instanceof Error ? error.message : String(error), stdout: null }
263
265
  }
@@ -355,7 +357,7 @@ function scanBinaryBuffer({ buffer, filePath, rules, exceptions, repo }) {
355
357
  return findings
356
358
  }
357
359
 
358
- function readBinaryChanges({ repoRoot, revision, diff, rules, exceptions, repo }) {
360
+ function readBinaryChanges({ repoRoot, revision, diff, rules, exceptions, repo, gitExecutable, gitRunner }) {
359
361
  const findings = []
360
362
  const diagnostics = []
361
363
  let totalBytes = 0
@@ -363,7 +365,9 @@ function readBinaryChanges({ repoRoot, revision, diff, rules, exceptions, repo }
363
365
  const spec = revision === ':' ? `:${filePath}` : `${revision}:${filePath}`
364
366
  const result = gitOutputResult(repoRoot, ['show', spec], {
365
367
  encoding: 'buffer',
368
+ gitExecutable,
366
369
  maxBuffer: BINARY_FILE_MAX_BYTES + 1,
370
+ runner: gitRunner,
367
371
  })
368
372
  if (!result.ok) {
369
373
  diagnostics.push(incompleteDiagnostic({
@@ -403,9 +407,10 @@ export function scanStagedRepository({
403
407
  rules = DEFAULT_CONTENT_RULES,
404
408
  exceptions = [],
405
409
  repo = null,
410
+ gitExecutable = 'git',
406
411
  gitRunner = spawnSync,
407
412
  } = {}) {
408
- const acquired = stagedDiff(repoRoot, { runner: gitRunner })
413
+ const acquired = stagedDiff(repoRoot, { gitExecutable, runner: gitRunner })
409
414
  if (!acquired.ok) {
410
415
  return {
411
416
  findings: [],
@@ -414,7 +419,7 @@ export function scanStagedRepository({
414
419
  }
415
420
  }
416
421
  const files = parseAddedContent(acquired.diff)
417
- const binary = readBinaryChanges({ repoRoot, revision: ':', diff: acquired.diff, rules, exceptions, repo })
422
+ const binary = readBinaryChanges({ repoRoot, revision: ':', diff: acquired.diff, rules, exceptions, repo, gitExecutable, gitRunner })
418
423
  return {
419
424
  findings: [...scanAddedContent({ files, rules, exceptions, repo }), ...binary.findings],
420
425
  diagnostics: binary.diagnostics,
@@ -4,6 +4,7 @@ import path from 'node:path'
4
4
  import { buildGraph } from '../graph/graph.mjs'
5
5
  import { commandProject, firstString, parseArgs, readJson, resolvePathValue, writeJson } from '../project/config.mjs'
6
6
  import { matchesPathPattern } from '../project/path-match.mjs'
7
+ import { sanitizedGitEnvironment } from '../runtime/git-adapter.mjs'
7
8
  import {
8
9
  CHECK_AGGREGATE_MAX_BYTES,
9
10
  parsePushRefInput,
@@ -206,16 +207,16 @@ export function validateBoundaryPolicy(policy, project = null) {
206
207
  return errors
207
208
  }
208
209
 
209
- export function resolveCurrentActor({ policy, project, actor = null, env = process.env } = {}) {
210
+ export function resolveCurrentActor({ policy, project, actor = null, env = process.env, gitExecutable = 'git', allowNetworkActorResolution = true, allowHistoryActorResolution = true } = {}) {
210
211
  const actors = policy?.actors ?? {}
211
212
  const explicit = actor || env.MNSTRY_ATELIER_ACTOR || env.GITHUB_ACTOR
212
213
  if (explicit && actors[explicit]) return { actorId: explicit, source: 'explicit' }
213
- const gitEmails = gitEmailsForProject(project)
214
+ const gitEmails = gitEmailsForProject(project, { gitExecutable, env, allowHistoryActorResolution })
214
215
  for (const [actorId, info] of Object.entries(actors)) {
215
216
  const actorEmails = new Set(asArray(info.gitEmails).map((email) => email.toLowerCase()))
216
217
  if (gitEmails.some((email) => actorEmails.has(email.toLowerCase()))) return { actorId, source: 'git-email', gitEmails }
217
218
  }
218
- const login = env.GITHUB_ACTOR || ghLogin()
219
+ const login = env.GITHUB_ACTOR || (allowNetworkActorResolution ? ghLogin() : null)
219
220
  if (login) {
220
221
  for (const [actorId, info] of Object.entries(actors)) {
221
222
  if (String(info.githubLogin || '').toLowerCase() === String(login).toLowerCase()) return { actorId, source: 'github-login', githubLogin: login }
@@ -224,16 +225,15 @@ export function resolveCurrentActor({ policy, project, actor = null, env = proce
224
225
  return { actorId: null, source: 'unverified', gitEmails, githubLogin: login || null }
225
226
  }
226
227
 
227
- function gitEmailsForProject(project) {
228
+ function gitEmailsForProject(project, { gitExecutable = 'git', env = process.env, allowHistoryActorResolution = true } = {}) {
228
229
  const roots = unique([project?.repoOpsRoot, project?.workspaceRoot, ...managedRepos(project).map((repo) => repo.path)])
229
230
  const emails = []
230
231
  for (const root of roots) {
231
232
  if (!root || !fs.existsSync(root)) continue
232
- for (const args of [
233
- ['config', 'user.email'],
234
- ['log', '-1', '--format=%ae'],
235
- ]) {
236
- const result = spawnSync('git', ['-C', root, ...args], { encoding: 'utf8' })
233
+ const probes = [['config', 'user.email']]
234
+ if (allowHistoryActorResolution) probes.push(['log', '-1', '--format=%ae'])
235
+ for (const args of probes) {
236
+ const result = spawnSync(gitExecutable, ['-C', root, ...args], { encoding: 'utf8', env: sanitizedGitEnvironment(env) })
237
237
  if (result.status === 0 && result.stdout.trim()) emails.push(result.stdout.trim())
238
238
  }
239
239
  }
@@ -280,10 +280,10 @@ function nodePlacementFindings({ node, policy }) {
280
280
  return findings
281
281
  }
282
282
 
283
- function actorFindings({ policy, project, actor }) {
283
+ function actorFindings({ policy, project, actor, gitExecutable, allowNetworkActorResolution, allowHistoryActorResolution, forceActorErrors }) {
284
284
  const findings = []
285
- const current = resolveCurrentActor({ policy, project, actor })
286
- const severity = severityFor(policy)
285
+ const current = resolveCurrentActor({ policy, project, actor, gitExecutable, allowNetworkActorResolution, allowHistoryActorResolution })
286
+ const severity = forceActorErrors ? 'error' : severityFor(policy)
287
287
  for (const [repoName, repo] of Object.entries(policy.repos ?? {})) {
288
288
  if (repo.kind !== 'private_domain') continue
289
289
  if (!repo.ownerActor) continue
@@ -296,11 +296,11 @@ function actorFindings({ policy, project, actor }) {
296
296
  return findings
297
297
  }
298
298
 
299
- export function stagedPathsForProject(project) {
299
+ export function stagedPathsForProject(project, { gitExecutable = 'git' } = {}) {
300
300
  const paths = []
301
301
  for (const repo of managedRepos(project)) {
302
302
  if (!repo.path || !fs.existsSync(path.join(repo.path, '.git'))) continue
303
- const result = spawnSync('git', ['-C', repo.path, 'diff', '--cached', '--name-only', '--diff-filter=ACMR'], { encoding: 'utf8' })
303
+ const result = spawnSync(gitExecutable, ['-C', repo.path, 'diff', '--cached', '--name-only', '--diff-filter=ACMR'], { encoding: 'utf8', env: sanitizedGitEnvironment() })
304
304
  if (result.status !== 0) continue
305
305
  for (const rel of result.stdout.split('\n').map((line) => line.trim()).filter(Boolean)) {
306
306
  paths.push({ repo: repo.name, repoRoot: repo.path, path: normalize(rel) })
@@ -394,7 +394,7 @@ export function semanticChangesInFile(file) {
394
394
  return changes
395
395
  }
396
396
 
397
- function semanticDiffFindings({ policy, project }) {
397
+ function semanticDiffFindings({ policy, project, gitExecutable = 'git' }) {
398
398
  const findings = []
399
399
  const severity = 'error'
400
400
  for (const repo of managedRepos(project)) {
@@ -402,7 +402,7 @@ function semanticDiffFindings({ policy, project }) {
402
402
  findings.push(unscannableRepoFinding(repo, 'configured path is absent or is not a Git checkout'))
403
403
  continue
404
404
  }
405
- const result = spawnSync('git', ['-C', repo.path, 'diff', '--cached', '--unified=0', '--', '*.md', '*.kg.json'], { encoding: 'utf8' })
405
+ const result = spawnSync(gitExecutable, ['-C', repo.path, 'diff', '--cached', '--unified=0', '--', '*.md', '*.kg.json'], { encoding: 'utf8', env: sanitizedGitEnvironment() })
406
406
  if (result.status !== 0) {
407
407
  findings.push(unscannableRepoFinding(repo, 'git diff --cached failed'))
408
408
  continue
@@ -430,7 +430,7 @@ function semanticDiffFindings({ policy, project }) {
430
430
  return findings
431
431
  }
432
432
 
433
- function stagedContentFindings({ policy, project }) {
433
+ function stagedContentFindings({ policy, project, gitExecutable = 'git' }) {
434
434
  const rules = resolveContentRules(policy)
435
435
  const exceptions = asArray(policy?.contentRuleExceptions)
436
436
  const findings = []
@@ -440,7 +440,7 @@ function stagedContentFindings({ policy, project }) {
440
440
  findings.push(unscannableRepoFinding(repo, 'configured path is absent or is not a Git checkout'))
441
441
  continue
442
442
  }
443
- const result = scanStagedRepository({ repoRoot: repo.path, rules, exceptions, repo: repo.name })
443
+ const result = scanStagedRepository({ repoRoot: repo.path, rules, exceptions, repo: repo.name, gitExecutable })
444
444
  findings.push(...result.findings, ...result.diagnostics)
445
445
  totalBytes += result.bytes
446
446
  if (totalBytes > CHECK_AGGREGATE_MAX_BYTES) {
@@ -603,7 +603,7 @@ function promotionFindings({ policy, project, graph }) {
603
603
  return findings
604
604
  }
605
605
 
606
- export function checkBoundaryPolicy({ project, policy, staged = false, stagedOnly = false, actor = null } = {}) {
606
+ export function checkBoundaryPolicy({ project, policy, staged = false, stagedOnly = false, actor = null, gitExecutable = 'git', allowNetworkActorResolution = true, allowHistoryActorResolution = true, forceActorErrors = false } = {}) {
607
607
  const validationErrors = validateBoundaryPolicy(policy, project)
608
608
  let graph = null
609
609
  const findings = validationErrors.map((message) => finding({ severity: 'error', code: 'boundary-policy-invalid', message }))
@@ -613,12 +613,12 @@ export function checkBoundaryPolicy({ project, policy, staged = false, stagedOnl
613
613
  for (const node of graph.nodes ?? []) findings.push(...nodePlacementFindings({ node, policy }))
614
614
  findings.push(...promotionFindings({ policy, project, graph }))
615
615
  }
616
- findings.push(...actorFindings({ policy, project, actor }))
616
+ findings.push(...actorFindings({ policy, project, actor, gitExecutable, allowNetworkActorResolution, allowHistoryActorResolution, forceActorErrors }))
617
617
  if (staged) {
618
- const stagedPaths = stagedPathsForProject(project)
618
+ const stagedPaths = stagedPathsForProject(project, { gitExecutable })
619
619
  findings.push(...forbiddenPathFindings({ policy, stagedPaths }))
620
- findings.push(...semanticDiffFindings({ policy, project }))
621
- findings.push(...stagedContentFindings({ policy, project }))
620
+ findings.push(...semanticDiffFindings({ policy, project, gitExecutable }))
621
+ findings.push(...stagedContentFindings({ policy, project, gitExecutable }))
622
622
  }
623
623
  const errors = findings.filter((item) => item.severity === 'error')
624
624
  const warnings = findings.filter((item) => item.severity !== 'error')
package/src/cli/run.mjs CHANGED
@@ -58,6 +58,7 @@ export const commandMap = new Map([
58
58
  ['feedback:check', ['src/commands/feedback.mjs', 'check']],
59
59
  ['announcements', ['src/commands/announcements.mjs']],
60
60
  ['announcements:list', ['src/commands/announcements.mjs', 'list']],
61
+ ['sync', ['src/commands/sync.mjs']],
61
62
  ])
62
63
 
63
64
  function isDefaultBrand(brand) {
@@ -115,6 +116,12 @@ Core commands:
115
116
  support bundle --dry-run Preview a no-send support bundle.
116
117
  feedback --message TEXT Write a local, never-sent feedback report.
117
118
  announcements list List and verify MNSTRY announcements.
119
+ sync enroll Explicitly enroll one repository for supervision.
120
+ sync status Fully observe the enrolled repository.
121
+ sync reconcile Fetch and perform only proven fast-forwards.
122
+ sync plan Prepare one bounded, reviewable commit plan.
123
+ sync commit Execute an exactly confirmed commit plan.
124
+ sync run --once Run one full-state supervisor cycle.
118
125
  egress check Check extracted Atelier paths for forbidden egress.
119
126
  boundary check Enforce private/shared repo placement rules.
120
127
  boundary audit Report content-rule matches tree-wide without blocking.
@@ -198,6 +205,17 @@ Assembles a local feedback report under ignored .atelier-local/feedback/ (mode 0
198
205
  announcements: `Usage: ${c} announcements list [--dir DIR] [--public-key FILE] | verify <file> [--public-key FILE] [--json] | show <file> [--public-key FILE]
199
206
 
200
207
  MNSTRY announcements are a pull-only channel: signed JSON documents under announcements/ in the repository. The trust anchor is always the committed MNSTRY key, or one you pass explicitly with --public-key; --dir changes only where documents are read from and never which key verifies them. Every run names the key and keyId it used. The kit never fetches anything — receiving announcements is the git pull you chose to run, and show refuses to print a body whose signature does not verify.`,
208
+ sync: `Usage:
209
+ ${c} sync enroll --repo DIR [--project atelier.project.json] [--git ABSOLUTE_PATH]
210
+ ${c} sync status --repo DIR
211
+ ${c} sync reconcile --repo DIR [--retries 3]
212
+ ${c} sync run --repo DIR [--once] [--interval 30]
213
+ ${c} sync plan --repo DIR --path FILE [--path FILE] --message TEXT [--publish]
214
+ ${c} sync commit --repo DIR --operation ID --confirm ID
215
+ ${c} sync pause|freeze|resume --repo DIR
216
+ ${c} sync trace --repo DIR
217
+
218
+ Enrolls exactly one repository and keeps Git plus readable files authoritative. Reconciliation observes the complete repository every cycle and performs only fast-forward updates. Commit creation is a two-phase user-confirmed operation: plan shows one bounded change set, and commit refuses unless the repository is unchanged and --confirm exactly repeats the operation id. No semantic conflict resolution, force operation, browser apply endpoint, telemetry, or desktop shell is present.`,
201
219
  }
202
220
  return help[command] || `Usage: ${c} ${command} [args]\n\nRun ${c} --help for the command list.`
203
221
  }
@@ -0,0 +1,100 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ enrollRepository,
4
+ executeUserConfirmedCommit,
5
+ operationTrace,
6
+ planUserConfirmedCommit,
7
+ reconcileRepository,
8
+ runtimeStatus,
9
+ setRepositoryPaused,
10
+ } from '../runtime/supervisor.mjs'
11
+ import { firstString, parseArgs } from '../project/config.mjs'
12
+
13
+ function valuesFor(argv, name) {
14
+ const values = []
15
+ for (let index = 0; index < argv.length; index += 1) {
16
+ const arg = argv[index]
17
+ if (arg === `--${name}` && argv[index + 1] && !argv[index + 1].startsWith('--')) {
18
+ values.push(argv[index + 1])
19
+ index += 1
20
+ } else if (arg.startsWith(`--${name}=`)) {
21
+ values.push(arg.slice(name.length + 3))
22
+ }
23
+ }
24
+ return values
25
+ }
26
+
27
+ function integer(value, fallback) {
28
+ if (value == null || value === true) return fallback
29
+ const parsed = Number(value)
30
+ if (!Number.isInteger(parsed) || parsed < 1) throw new Error('interval and retry values must be positive integers')
31
+ return parsed
32
+ }
33
+
34
+ function print(value) {
35
+ console.log(JSON.stringify(value, null, 2))
36
+ }
37
+
38
+ async function runLoop({ repoPath, intervalMs, fetchAttempts, once }) {
39
+ do {
40
+ const result = reconcileRepository({ repoPath, fetchAttempts })
41
+ print(result)
42
+ if (once) return result.ok ? 0 : 1
43
+ await new Promise((resolve) => setTimeout(resolve, intervalMs))
44
+ } while (true)
45
+ }
46
+
47
+ const argv = process.argv.slice(2)
48
+ const args = parseArgs(argv)
49
+ const subcommand = args._[0] || 'status'
50
+ const repoPath = firstString(args.repo) || process.cwd()
51
+
52
+ try {
53
+ if (subcommand === 'enroll') {
54
+ const result = enrollRepository({ repoPath, projectConfig: firstString(args.project, args['project-config']), gitExecutable: firstString(args.git) })
55
+ print(result)
56
+ process.exitCode = result.ok ? 0 : 1
57
+ } else if (subcommand === 'status' || subcommand === 'audit') {
58
+ const result = runtimeStatus({ repoPath })
59
+ print(result)
60
+ process.exitCode = result.ok ? 0 : 1
61
+ } else if (subcommand === 'reconcile') {
62
+ const result = reconcileRepository({ repoPath, fetchAttempts: integer(args.retries, 3) })
63
+ print(result)
64
+ process.exitCode = result.ok ? 0 : 1
65
+ } else if (subcommand === 'run') {
66
+ process.exitCode = await runLoop({
67
+ repoPath,
68
+ intervalMs: integer(args.interval, 30) * 1000,
69
+ fetchAttempts: integer(args.retries, 3),
70
+ once: Boolean(args.once),
71
+ })
72
+ } else if (subcommand === 'plan') {
73
+ print(planUserConfirmedCommit({
74
+ repoPath,
75
+ paths: valuesFor(argv, 'path'),
76
+ message: firstString(args.message),
77
+ publish: Boolean(args.publish),
78
+ fetchAttempts: integer(args.retries, 3),
79
+ }))
80
+ } else if (subcommand === 'commit') {
81
+ const result = executeUserConfirmedCommit({
82
+ repoPath,
83
+ operationId: firstString(args.operation),
84
+ confirmation: firstString(args.confirm),
85
+ })
86
+ print(result)
87
+ process.exitCode = result.ok ? 0 : 1
88
+ } else if (subcommand === 'pause' || subcommand === 'freeze') {
89
+ print(setRepositoryPaused({ repoPath, paused: true, reason: firstString(args.reason) || (subcommand === 'freeze' ? 'frozen by user' : 'paused by user') }))
90
+ } else if (subcommand === 'resume') {
91
+ print(setRepositoryPaused({ repoPath, paused: false }))
92
+ } else if (subcommand === 'trace') {
93
+ print({ ok: true, operations: operationTrace({ repoPath }) })
94
+ } else {
95
+ throw new Error(`unknown sync command: ${subcommand}`)
96
+ }
97
+ } catch (error) {
98
+ console.error(error.message)
99
+ process.exitCode = 1
100
+ }
@@ -26,6 +26,12 @@ import { fileURLToPath } from 'node:url'
26
26
  // document, not a schema, and is deliberately absent.
27
27
 
28
28
  export const CONTRACT_CORPUS = [
29
+ {
30
+ name: 'atelier-repository-observation',
31
+ contractFile: 'contracts/atelier-repository-observation.v1.schema.json',
32
+ fixtureRoot: 'fixtures/atelier-repository-observation',
33
+ registry: true,
34
+ },
29
35
  {
30
36
  name: 'atelier-lock',
31
37
  contractFile: 'contracts/atelier-lock.v1.schema.json',
package/src/index.mjs CHANGED
@@ -98,3 +98,32 @@ export {
98
98
  export {
99
99
  analysisAdapterDryRun,
100
100
  } from './analysis/adapter.mjs'
101
+
102
+ export {
103
+ GitCommandError,
104
+ classifyRemoteAuthentication,
105
+ inspectGitEngine,
106
+ parseGitVersion,
107
+ resolveGitExecutable,
108
+ runGit,
109
+ sanitizeRemoteUrl,
110
+ } from './runtime/git-adapter.mjs'
111
+
112
+ export {
113
+ ATELIER_REPOSITORY_OBSERVATION_SCHEMA,
114
+ classifyFilesystemRoot,
115
+ observeRepository,
116
+ resolveRepositoryRoot,
117
+ validateRepositoryObservation,
118
+ } from './runtime/repository-observation.mjs'
119
+
120
+ export {
121
+ ATELIER_COMMIT_PLAN_SCHEMA,
122
+ enrollRepository,
123
+ executeUserConfirmedCommit,
124
+ operationTrace,
125
+ planUserConfirmedCommit,
126
+ reconcileRepository,
127
+ runtimeStatus,
128
+ setRepositoryPaused,
129
+ } from './runtime/supervisor.mjs'