@atolis-hq/wake 0.2.1 → 0.2.2

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/README.md CHANGED
@@ -97,7 +97,7 @@ For more detail, see [docs/vision.md](docs/vision.md) and
97
97
  log; projections can be rebuilt, and the loop can crash and resume without
98
98
  losing its place.
99
99
  - **Local and inspectable.** Everything lives in a plain-file Wake home
100
- directory: `config.json`, `prompts/`, and `workspaces/` at the top level for
100
+ directory: `config.yaml`, `config.workflows.yaml`, `prompts/`, and `workspaces/` at the top level for
101
101
  what you edit or browse day-to-day, with durable/internal state (events,
102
102
  projections, runs, logs, sandbox auth) nested under a hidden `.wake/`.
103
103
  - **Sandbox-oriented execution.** Wake can run from a persistent Docker sandbox
@@ -169,7 +169,7 @@ any time for the full command list, or see
169
169
  - [docs/architecture.md](docs/architecture.md) — module boundaries and the event-sourced core.
170
170
  - [docs/workflows.md](docs/workflows.md) — how stages, prompts, and runner routes are configured.
171
171
  - [docs/prompts.md](docs/prompts.md) — how prompt templates map to workflow stages.
172
- - [docs/configuration.md](docs/configuration.md) — `config.json` options and the operator correlation escape hatch.
172
+ - [docs/configuration.md](docs/configuration.md) — `config.yaml`/`config.workflows.yaml` options and the operator correlation escape hatch.
173
173
  - [docs/development.md](docs/development.md) — source-checkout dev setup (`wake-dev`), npm scripts, formatting, self-update, GitHub polling.
174
174
  - [docs/runner-comparison.md](docs/runner-comparison.md) — capability differences between supported runners.
175
175
 
@@ -1,6 +1,6 @@
1
1
  import { access, appendFile, mkdir, readFile, readdir, rename } from 'node:fs/promises';
2
2
  import { dirname, join } from 'node:path';
3
- import { parseEventEnvelope, parseIssueStateRecord, parseLedger, parseRunRecord, parseSourceStateRecord, parseWakeConfig, } from '../../domain/schema.js';
3
+ import { parseEventEnvelope, parseIssueStateRecord, parseLedger, parseRunRecord, parseSourceStateRecord, } from '../../domain/schema.js';
4
4
  import { isTerminalStage } from '../../domain/stages.js';
5
5
  import { appendJsonLine, readJsonFile, writeJsonFile } from '../../lib/json-file.js';
6
6
  import { createWakePaths } from '../../lib/paths.js';
@@ -144,11 +144,6 @@ export function createStateStore({ wakeRoot }) {
144
144
  async ensureWakeRoot() {
145
145
  await mkdir(wakeRoot, { recursive: true });
146
146
  },
147
- async writeConfig(record) {
148
- const parsed = parseWakeConfig(record);
149
- await writeJsonFile(paths.configFile, parsed);
150
- return parsed;
151
- },
152
147
  async writeLedger(record) {
153
148
  const parsed = parseLedger(record);
154
149
  await writeJsonFile(paths.ledgerFile, parsed);
@@ -1,7 +1,8 @@
1
1
  import { access, chmod, copyFile, mkdir, readFile, readdir, writeFile } from 'node:fs/promises';
2
2
  import { basename, dirname, join, resolve } from 'node:path';
3
3
  import { createDefaultWakeConfig } from '../config/defaults.js';
4
- import { writeJsonFile } from '../lib/json-file.js';
4
+ import { splitWakeConfig } from '../config/split-config.js';
5
+ import { writeYamlFile } from '../lib/yaml-file.js';
5
6
  import { createWakePaths } from '../lib/paths.js';
6
7
  const promptFileNames = ['refine.md', 'implement.md'];
7
8
  function sanitizeContainerName(name) {
@@ -85,8 +86,10 @@ export async function scaffoldWakeHome(input) {
85
86
  paths.workspaceRoot,
86
87
  ];
87
88
  await Promise.all(runtimeDirectories.map((directoryPath) => mkdir(directoryPath, { recursive: true })));
89
+ const { infra, workflow } = splitWakeConfig(config);
88
90
  await Promise.all([
89
91
  copyAssets(repoRoot, 'prompts', join(wakeRoot, 'prompts'), promptFileNames),
90
- writeJsonFile(join(wakeRoot, 'config.json'), config),
92
+ writeYamlFile(join(wakeRoot, 'config.yaml'), infra),
93
+ writeYamlFile(join(wakeRoot, 'config.workflows.yaml'), workflow),
91
94
  ]);
92
95
  }
@@ -0,0 +1,11 @@
1
+ import { readdir } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ const configFileNamePattern = /^config(\..+)?\.yaml$/;
4
+ export async function discoverConfigFiles(wakeRoot) {
5
+ const entries = await readdir(wakeRoot, { withFileTypes: true }).catch(() => []);
6
+ return entries
7
+ .filter((entry) => entry.isFile() && configFileNamePattern.test(entry.name))
8
+ .map((entry) => entry.name)
9
+ .sort()
10
+ .map((name) => join(wakeRoot, name));
11
+ }
@@ -1,20 +1,47 @@
1
- import { resolve } from 'node:path';
1
+ import { resolve, join } from 'node:path';
2
2
  import { access } from 'node:fs/promises';
3
3
  import { readJsonFile } from '../lib/json-file.js';
4
+ import { readYamlFile } from '../lib/yaml-file.js';
5
+ import { deepMergeRaw } from '../lib/deep-merge.js';
4
6
  import { parseWakeConfig } from '../domain/schema.js';
7
+ import { discoverConfigFiles } from './discover-config-files.js';
8
+ async function readLegacyConfigIfPresent(wakeRoot) {
9
+ const legacyConfigFile = join(wakeRoot, 'config.json');
10
+ try {
11
+ await access(legacyConfigFile);
12
+ }
13
+ catch {
14
+ return {};
15
+ }
16
+ return readJsonFile(legacyConfigFile);
17
+ }
5
18
  export async function loadWakeConfig(options) {
6
19
  const wakeRoot = options?.wakeRoot ?? resolve(process.cwd(), '.wake');
7
- const configFile = options?.configFile;
8
- let raw = {};
9
- if (configFile !== undefined) {
10
- try {
11
- await access(configFile);
12
- raw = await readJsonFile(configFile);
13
- }
14
- catch {
15
- // no config file schema defaults apply
20
+ const configFiles = await discoverConfigFiles(wakeRoot);
21
+ let raw;
22
+ if (configFiles.length > 0) {
23
+ raw = {};
24
+ for (const configFile of configFiles) {
25
+ let parsed;
26
+ try {
27
+ // yaml.parse returns null for empty/comment-only files rather than
28
+ // {}, so coalesce before merging.
29
+ parsed = (await readYamlFile(configFile)) ?? {};
30
+ }
31
+ catch (error) {
32
+ throw new Error(`Failed to parse ${configFile}: ${error.message}`, {
33
+ cause: error,
34
+ });
35
+ }
36
+ raw = deepMergeRaw(raw, parsed);
16
37
  }
17
38
  }
39
+ else {
40
+ // Pre-split Wake homes only have a single config.json — Wake reads it
41
+ // directly rather than requiring a migration step. It stays untouched
42
+ // on disk; nothing here writes it back out (see docs/configuration.md).
43
+ raw = await readLegacyConfigIfPresent(wakeRoot);
44
+ }
18
45
  // wakeRoot is always the live invocation's --wake-root/cwd, never a value
19
46
  // to accept from a (possibly stale, possibly container-context) config
20
47
  // file — spread rawPaths first so wakeRoot always wins. promptsRoot and
@@ -0,0 +1,14 @@
1
+ import { wakeInfraConfigSchema, wakeWorkflowConfigSchema } from '../domain/schema.js';
2
+ const infraKeys = Object.keys(wakeInfraConfigSchema.shape);
3
+ const workflowKeys = Object.keys(wakeWorkflowConfigSchema.shape);
4
+ export function splitWakeConfig(config) {
5
+ const infra = {};
6
+ for (const key of infraKeys) {
7
+ infra[key] = config[key];
8
+ }
9
+ const workflow = {};
10
+ for (const key of workflowKeys) {
11
+ workflow[key] = config[key];
12
+ }
13
+ return { infra, workflow };
14
+ }
@@ -391,8 +391,7 @@ export const ledgerSchema = z.object({
391
391
  lastQuotaFailureAt: isoTimestampSchema.optional(),
392
392
  runners: z.record(z.string(), runnerHealthEntrySchema).default({}),
393
393
  });
394
- export const wakeConfigSchema = z
395
- .object({
394
+ const wakeConfigBaseSchema = z.object({
396
395
  schemaVersion: z.literal(1).default(1),
397
396
  paths: z.object({
398
397
  wakeRoot: z.string(),
@@ -649,8 +648,28 @@ export const wakeConfigSchema = z
649
648
  },
650
649
  }),
651
650
  sinks: z.record(z.string(), sinkEntrySchema).default({}),
652
- })
653
- .superRefine((config, ctx) => {
651
+ });
652
+ export const wakeInfraConfigSchema = wakeConfigBaseSchema.pick({
653
+ schemaVersion: true,
654
+ paths: true,
655
+ sandbox: true,
656
+ dev: true,
657
+ scheduler: true,
658
+ transcripts: true,
659
+ ui: true,
660
+ sources: true,
661
+ sinks: true,
662
+ });
663
+ export const wakeWorkflowConfigSchema = wakeConfigBaseSchema.pick({
664
+ runners: true,
665
+ tiers: true,
666
+ defaultTier: true,
667
+ workflows: true,
668
+ workflowSelectors: true,
669
+ commands: true,
670
+ stages: true,
671
+ });
672
+ export const wakeConfigSchema = wakeConfigBaseSchema.superRefine((config, ctx) => {
654
673
  const promptsRoot = config.paths.promptsRoot ?? defaultPromptsRoot();
655
674
  const workflowEntries = Object.entries(config.workflows);
656
675
  const commandEntries = Object.entries(config.commands);
@@ -0,0 +1,14 @@
1
+ function isPlainObject(value) {
2
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
3
+ }
4
+ export function deepMergeRaw(target, source) {
5
+ const result = { ...target };
6
+ for (const [key, sourceValue] of Object.entries(source)) {
7
+ const targetValue = result[key];
8
+ result[key] =
9
+ isPlainObject(sourceValue) && isPlainObject(targetValue)
10
+ ? deepMergeRaw(targetValue, sourceValue)
11
+ : sourceValue;
12
+ }
13
+ return result;
14
+ }
@@ -11,7 +11,8 @@ export function createWakePaths(wakeRoot) {
11
11
  wakeRoot,
12
12
  dataRoot,
13
13
  containerHomeRoot: join(dataRoot, 'container-home'),
14
- configFile: join(wakeRoot, 'config.json'),
14
+ configFile: join(wakeRoot, 'config.yaml'),
15
+ workflowsConfigFile: join(wakeRoot, 'config.workflows.yaml'),
15
16
  ledgerFile: join(dataRoot, 'ledger.json'),
16
17
  pauseFile: join(dataRoot, 'PAUSE'),
17
18
  tickRequestFile: join(dataRoot, 'control', 'tick-request.json'),
@@ -0,0 +1,20 @@
1
+ import { mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { dirname } from 'node:path';
4
+ import { parse, stringify } from 'yaml';
5
+ export async function writeYamlFile(path, value) {
6
+ await mkdir(dirname(path), { recursive: true });
7
+ const tempPath = `${path}.${randomUUID()}.tmp`;
8
+ try {
9
+ await writeFile(tempPath, stringify(value), 'utf8');
10
+ await rename(tempPath, path);
11
+ }
12
+ catch (error) {
13
+ await rm(tempPath, { force: true }).catch(() => { });
14
+ throw error;
15
+ }
16
+ }
17
+ export async function readYamlFile(path) {
18
+ const raw = await readFile(path, 'utf8');
19
+ return parse(raw);
20
+ }
package/dist/src/main.js CHANGED
@@ -441,9 +441,7 @@ export async function buildRuntime(args) {
441
441
  await stateStore.ensureWakeRoot();
442
442
  const config = await loadWakeConfig({
443
443
  wakeRoot,
444
- configFile: stateStore.paths.configFile,
445
444
  });
446
- await stateStore.writeConfig(config);
447
445
  const resourceIndex = createResourceIndex({ paths: stateStore.paths });
448
446
  const prTrackingEnabled = config.sources.github.enabled && config.sources.github.pullRequests.enabled;
449
447
  // Resolved once and shared: resolveGitHubToken shells out to `gh auth
@@ -610,7 +608,6 @@ async function runUi(args) {
610
608
  await stateStore.ensureWakeRoot();
611
609
  const config = await loadWakeConfig({
612
610
  wakeRoot,
613
- configFile: stateStore.paths.configFile,
614
611
  });
615
612
  const server = await runUiCommand({
616
613
  args,
@@ -659,7 +656,6 @@ async function runDoctor(args) {
659
656
  await stateStore.ensureWakeRoot();
660
657
  const config = await loadWakeConfig({
661
658
  wakeRoot,
662
- configFile: stateStore.paths.configFile,
663
659
  });
664
660
  const docker = createHostDockerCli();
665
661
  const packageRoot = resolvePackageRoot();
@@ -811,7 +807,6 @@ async function main() {
811
807
  await stateStore.ensureWakeRoot();
812
808
  const config = await loadWakeConfig({
813
809
  wakeRoot,
814
- configFile: stateStore.paths.configFile,
815
810
  });
816
811
  const docker = createHostDockerCli();
817
812
  const repoRoot = config.dev?.repoRoot;
@@ -928,7 +923,6 @@ async function main() {
928
923
  await stateStore.ensureWakeRoot();
929
924
  const config = await loadWakeConfig({
930
925
  wakeRoot,
931
- configFile: stateStore.paths.configFile,
932
926
  });
933
927
  const wakeInvocation = config.dev?.mode === 'source' ? ['node', '/app/dist/src/main.js'] : ['wake'];
934
928
  await runSandbox(['exec', '--', ...wakeInvocation, ...rewritten]);
@@ -1 +1 @@
1
- export const wakeVersion = "0.2.1+g4279d3f";
1
+ export const wakeVersion = "0.2.2+g83e9f9a";
@@ -87,7 +87,7 @@ prepare_codex_home
87
87
  mirror_stdout "[${label}] begin ts=$(date -u +%FT%TZ) cwd=$(pwd) command=$*"
88
88
  mirror_stdout "[${label}] paths hostWakeRoot=${host_wake_root} containerWakeRoot=${container_wake_root} promptsRoot=${prompts_root} containerHome=${container_home} hostContainerHome=${host_container_home} containerMount=${container_mount} containerName=${container_name}"
89
89
 
90
- emit_check "wake-config" test -f "${container_mount}/config.json"
90
+ emit_check "wake-config" test -f "${container_mount}/config.yaml" -o -f "${container_mount}/config.json"
91
91
  emit_check "prompts-root" test -d "${prompts_root}"
92
92
  emit_check "workspaces-root" test -d "${container_mount}/workspaces"
93
93
  emit_check "repos-root" test -d "${container_mount}/repos"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atolis-hq/wake",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "Local autonomous agent control plane for software development",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -48,6 +48,7 @@
48
48
  "@octokit/rest": "^22.0.0",
49
49
  "handlebars": "^4.7.9",
50
50
  "ulid": "^3.0.2",
51
+ "yaml": "^2.9.0",
51
52
  "zod": "^4.1.5"
52
53
  },
53
54
  "devDependencies": {