@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/CHANGELOG.md +15 -0
- package/README.md +36 -19
- package/contracts/atelier-repository-observation.v1.schema.json +163 -0
- package/docs/assurance-controls.md +4 -2
- package/docs/atelier-runtime.md +13 -2
- package/docs/atelier-sync.md +171 -0
- package/docs/blocks/claims.md +18 -13
- package/docs/blocks/will-not-do.md +9 -3
- package/docs/install.md +3 -3
- package/docs/release-engineering.md +11 -2
- package/docs/upgrade.md +17 -2
- 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/package.json +5 -1
- package/src/boundary/content-rules.mjs +9 -4
- package/src/boundary/policy.mjs +23 -23
- package/src/cli/run.mjs +18 -0
- package/src/commands/sync.mjs +100 -0
- package/src/contracts/corpus.mjs +6 -0
- package/src/index.mjs +29 -0
- package/src/project/config.mjs +27 -25
- package/src/runtime/git-adapter.mjs +189 -0
- package/src/runtime/local-state.mjs +439 -0
- package/src/runtime/repository-observation.mjs +491 -0
- package/src/runtime/supervisor.mjs +788 -0
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.
|
|
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.
|
|
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.
|
|
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(
|
|
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,
|
package/src/boundary/policy.mjs
CHANGED
|
@@ -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
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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(
|
|
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(
|
|
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
|
+
}
|
package/src/contracts/corpus.mjs
CHANGED
|
@@ -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'
|