wogiflow 2.18.1 → 2.19.0

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/bin/flow CHANGED
@@ -218,6 +218,17 @@ function main() {
218
218
  return;
219
219
  }
220
220
 
221
+ if (command === 'config') {
222
+ const { configCommand } = require('../lib/commands/config');
223
+ try {
224
+ configCommand(args.slice(1));
225
+ } catch (err) {
226
+ console.error(`Config error: ${err.message}`);
227
+ process.exit(1);
228
+ }
229
+ return;
230
+ }
231
+
221
232
  // For all other commands, try to find project context
222
233
  const projectRoot = findProjectRoot();
223
234
 
@@ -0,0 +1,148 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * `flow config` — inspect and compact the project's .workflow/config.json.
5
+ *
6
+ * Subcommands:
7
+ * flow config show Show the current (on-disk) config as written
8
+ * flow config show --full Show the fully-merged config (defaults + overrides)
9
+ * flow config show --diff Show only the keys this project overrides
10
+ * flow config compact Rewrite config.json to lean form (overrides only)
11
+ * flow config compact --dry Preview the compacted form without writing
12
+ *
13
+ * Added in v2.19.0 alongside the lean-config-on-init change. See
14
+ * `lib/installer.js` `buildLeanInstallConfig()` for the write-time counterpart.
15
+ */
16
+
17
+ const fs = require('node:fs');
18
+ const path = require('node:path');
19
+
20
+ function resolveProjectRoot() {
21
+ let dir = process.cwd();
22
+ for (let i = 0; i < 20; i++) {
23
+ if (fs.existsSync(path.join(dir, '.workflow', 'config.json'))) return dir;
24
+ const parent = path.dirname(dir);
25
+ if (parent === dir) break;
26
+ dir = parent;
27
+ }
28
+ throw new Error('Not in a WogiFlow project (no .workflow/config.json found)');
29
+ }
30
+
31
+ function readProjectConfig(projectRoot) {
32
+ const configPath = path.join(projectRoot, '.workflow', 'config.json');
33
+ const raw = fs.readFileSync(configPath, 'utf-8');
34
+ return { configPath, raw, parsed: JSON.parse(raw) };
35
+ }
36
+
37
+ function configCommand(args) {
38
+ const sub = args[0];
39
+ const flags = args.slice(1);
40
+
41
+ if (!sub || sub === '--help' || sub === '-h') {
42
+ printHelp();
43
+ return;
44
+ }
45
+
46
+ const projectRoot = resolveProjectRoot();
47
+
48
+ if (sub === 'show') {
49
+ return showConfig(projectRoot, flags);
50
+ }
51
+
52
+ if (sub === 'compact') {
53
+ return compactConfig(projectRoot, flags);
54
+ }
55
+
56
+ console.error(`Unknown config subcommand: ${sub}`);
57
+ printHelp();
58
+ process.exit(1);
59
+ }
60
+
61
+ function showConfig(projectRoot, flags) {
62
+ const { configPath, parsed } = readProjectConfig(projectRoot);
63
+ const { mergeWithDefaults, computeLeanConfig } = require('../../scripts/flow-config-defaults');
64
+
65
+ if (flags.includes('--full')) {
66
+ const merged = mergeWithDefaults(parsed);
67
+ console.log(JSON.stringify(merged, null, 2));
68
+ return;
69
+ }
70
+
71
+ if (flags.includes('--diff')) {
72
+ const merged = mergeWithDefaults(parsed);
73
+ const lean = computeLeanConfig(merged);
74
+ console.log(JSON.stringify(lean, null, 2));
75
+ return;
76
+ }
77
+
78
+ // Default: show what's on disk.
79
+ const relPath = path.relative(projectRoot, configPath);
80
+ console.log(`# ${relPath} (on disk — ${Object.keys(parsed).length} top-level keys)`);
81
+ console.log(JSON.stringify(parsed, null, 2));
82
+ console.log('');
83
+ console.log('# Tips:');
84
+ console.log('# flow config show --full see the fully-merged config (defaults + overrides)');
85
+ console.log('# flow config show --diff see only the keys this project overrides');
86
+ console.log('# flow config compact shrink this file to overrides-only');
87
+ }
88
+
89
+ function compactConfig(projectRoot, flags) {
90
+ const { configPath, parsed } = readProjectConfig(projectRoot);
91
+ const { mergeWithDefaults, computeLeanConfig } = require('../../scripts/flow-config-defaults');
92
+
93
+ // mergeWithDefaults → computeLeanConfig round-trip produces the canonical
94
+ // lean form. Doing it via merge-first ensures any keys that used to have
95
+ // non-default values but now match a new default get correctly removed.
96
+ const merged = mergeWithDefaults(parsed);
97
+ const lean = computeLeanConfig(merged);
98
+
99
+ const beforeBytes = JSON.stringify(parsed, null, 2).length;
100
+ const afterBytes = JSON.stringify(lean, null, 2).length;
101
+ const savedPct = beforeBytes > 0 ? Math.round(((beforeBytes - afterBytes) / beforeBytes) * 100) : 0;
102
+
103
+ if (flags.includes('--dry') || flags.includes('--dry-run')) {
104
+ console.log('# Preview — would write:');
105
+ console.log(JSON.stringify(lean, null, 2));
106
+ console.log('');
107
+ console.log(`# Before: ${beforeBytes} bytes, ${Object.keys(parsed).length} top-level keys`);
108
+ console.log(`# After: ${afterBytes} bytes, ${Object.keys(lean).length} top-level keys (-${savedPct}%)`);
109
+ console.log('# Run without --dry to apply.');
110
+ return;
111
+ }
112
+
113
+ // Backup first — compaction is reversible, but cheap insurance is free.
114
+ const backupPath = `${configPath}.bak-${Date.now()}`;
115
+ fs.copyFileSync(configPath, backupPath);
116
+
117
+ fs.writeFileSync(configPath, JSON.stringify(lean, null, 2) + '\n');
118
+
119
+ console.log(`✓ Compacted ${path.relative(projectRoot, configPath)}`);
120
+ console.log(` Before: ${beforeBytes} bytes, ${Object.keys(parsed).length} top-level keys`);
121
+ console.log(` After: ${afterBytes} bytes, ${Object.keys(lean).length} top-level keys (-${savedPct}%)`);
122
+ console.log(` Backup: ${path.relative(projectRoot, backupPath)}`);
123
+ console.log('');
124
+ console.log('Runtime behavior is unchanged — defaults merge in at read time.');
125
+ console.log('Verify with: flow config show --full');
126
+ }
127
+
128
+ function printHelp() {
129
+ console.log(`Usage: flow config <subcommand> [flags]
130
+
131
+ Inspect and compact the project's .workflow/config.json.
132
+
133
+ Subcommands:
134
+ show Show the current config as written on disk
135
+ show --full Show the fully-merged config (defaults + overrides)
136
+ show --diff Show only the keys this project overrides
137
+ compact Rewrite config.json to overrides-only form
138
+ compact --dry Preview the compacted form without writing
139
+
140
+ Why "lean" configs (v2.19.0+):
141
+ The runtime config loader merges CONFIG_DEFAULTS on every read, so any key
142
+ matching the default is redundant noise. New installs write lean configs by
143
+ default. Existing fat configs still work identically — run 'flow config
144
+ compact' to shrink them.
145
+ `);
146
+ }
147
+
148
+ module.exports = { configCommand, showConfig, compactConfig, resolveProjectRoot };
package/lib/installer.js CHANGED
@@ -520,21 +520,37 @@ function createWorkflowStructure(projectRoot, config) {
520
520
  }
521
521
  }
522
522
 
523
- // Create config.json using shared defaults with project-specific overrides
523
+ // Create config.json LEAN by default (v2.19.0+).
524
+ // Only project-specific identity keys + any overrides the installer selected
525
+ // interactively are written. The runtime loader (flow-config-loader.js)
526
+ // merges CONFIG_DEFAULTS on every read via mergeWithDefaults(), so a lean
527
+ // file behaves identically to a fully-specified one.
528
+ //
529
+ // This replaces the prior behavior of dumping the full ~300-line default
530
+ // tree on every `flow init`, which was confusing (users thought they had
531
+ // to maintain all those keys) and brittle (every new default shipped in a
532
+ // release became stale in every existing project's config file).
524
533
  const configPath = path.join(workflowDir, 'config.json');
525
- const configContent = getDefaultConfig({
534
+ const fullConfig = getDefaultConfig({
526
535
  version: config.version,
527
536
  projectName: config.projectName,
528
537
  projectType: config.projectType || 'unknown',
529
538
  strictMode: config.strictMode
530
539
  });
531
- fs.writeFileSync(configPath, JSON.stringify(configContent, null, 2));
532
-
533
- // Sync .gitignore entries for enabled features
540
+ const leanConfig = buildLeanInstallConfig(fullConfig, config);
541
+ fs.writeFileSync(configPath, JSON.stringify(leanConfig, null, 2) + '\n');
542
+ // To inspect the fully-merged config (defaults + your overrides), run:
543
+ // flow config show --full
544
+ // To compact an existing fat config back to lean form, run:
545
+ // flow config compact
546
+
547
+ // Sync .gitignore entries for enabled features. Pass the fully-merged
548
+ // config (not the lean on-disk form) so gitignore can see all enabled
549
+ // feature flags, including those inherited from defaults.
534
550
  try {
535
551
  const { syncGitignore } = require('../scripts/flow-gitignore');
536
- syncGitignore(configContent);
537
- } catch (err) {
552
+ syncGitignore(fullConfig);
553
+ } catch (_err) {
538
554
  // Non-blocking — gitignore sync should never fail installation
539
555
  }
540
556
 
@@ -1118,4 +1134,42 @@ function walkDirForManifest(dir, baseDir, fileSet) {
1118
1134
  }
1119
1135
  }
1120
1136
 
1121
- module.exports = { init, getDefaultConfig, deepMerge, detectProjectType, detectProjectScripts, detectTsConfigMode, getRegistriesForProjectType };
1137
+ /**
1138
+ * Build the lean config written by `flow init` (v2.19.0+).
1139
+ *
1140
+ * The runtime loader merges CONFIG_DEFAULTS on every read, so we only need to
1141
+ * persist values that (a) identify this project or (b) differ from the default.
1142
+ * We prefer the `computeLeanConfig()` diff over a hand-picked allowlist so any
1143
+ * interactive-install overrides (e.g. strictMode toggled off) are preserved.
1144
+ *
1145
+ * @param {Object} fullConfig - The fully-merged config object from getDefaultConfig()
1146
+ * @param {Object} config - The install-time config selections (projectName, version, etc.)
1147
+ * @returns {Object} Lean config suitable for writing to .workflow/config.json
1148
+ */
1149
+ function buildLeanInstallConfig(fullConfig, config) {
1150
+ const { computeLeanConfig } = require('../scripts/flow-config-defaults');
1151
+ const lean = computeLeanConfig(fullConfig);
1152
+
1153
+ // Ensure identity keys are always present with the expected shape.
1154
+ const out = {
1155
+ $schema: './config.schema.json',
1156
+ version: config.version,
1157
+ projectName: config.projectName || '',
1158
+ cli: { type: 'claude-code' },
1159
+ ...lean
1160
+ };
1161
+
1162
+ // Strip identity keys from the lean block so they aren't duplicated.
1163
+ // (The spread above means lean's values would win; we want OURS to win.)
1164
+ return {
1165
+ $schema: out.$schema,
1166
+ version: out.version,
1167
+ projectName: out.projectName,
1168
+ cli: out.cli,
1169
+ ...Object.fromEntries(
1170
+ Object.entries(lean).filter(([k]) => !['$schema', 'version', 'projectName', 'cli'].includes(k))
1171
+ )
1172
+ };
1173
+ }
1174
+
1175
+ module.exports = { init, getDefaultConfig, deepMerge, detectProjectType, detectProjectScripts, detectTsConfigMode, getRegistriesForProjectType, buildLeanInstallConfig };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wogiflow",
3
- "version": "2.18.1",
3
+ "version": "2.19.0",
4
4
  "description": "AI-powered development workflow management system with multi-model support",
5
5
  "main": "lib/index.js",
6
6
  "bin": {
@@ -10,7 +10,7 @@
10
10
  },
11
11
  "scripts": {
12
12
  "flow": "./scripts/flow",
13
- "test": "NODE_ENV=test node --test tests/auto-compact-prompt.test.js tests/flow-paths.test.js tests/flow-io.test.js tests/flow-config-loader.test.js tests/flow-damage-control.test.js tests/flow-output.test.js tests/flow-constants.test.js tests/flow-session-state.test.js tests/flow-hooks-integration.test.js tests/flow-utils.test.js tests/flow-security.test.js tests/flow-memory-db.test.js tests/flow-durable-session.test.js tests/flow-skill-matcher.test.js tests/flow-bridge.test.js tests/flow-proactive-compact.test.js tests/flow-cascade-completion.test.js tests/flow-capture-gate.test.js tests/flow-correction-detector-hybrid.test.js tests/flow-promote.test.js tests/flow-archive-runs.test.js tests/flow-memory.test.js tests/flow-hooks-pre-tool-helpers.test.js tests/flow-hooks-bugfix-scope-gate.test.js tests/flow-hooks-routing-gate.test.js tests/flow-hooks-phase-read-gate.test.js tests/flow-hooks-commit-log-gate.test.js tests/flow-hooks-deploy-gate.test.js tests/flow-hooks-todowrite-gate.test.js tests/flow-hooks-git-safety-gate.test.js tests/flow-hooks-scope-mutation-gate.test.js tests/flow-hooks-strike-gate.test.js tests/flow-hooks-component-check.test.js tests/flow-hooks-scope-gate.test.js tests/flow-hooks-implementation-gate.test.js tests/flow-hooks-research-gate.test.js tests/flow-hooks-loop-check.test.js tests/flow-hooks-manager-boundary-gate.test.js tests/flow-hooks-phase-gate.test.js tests/flow-hooks-pre-tool-orchestrator.test.js tests/flow-hooks-observation-capture.test.js tests/flow-hooks-task-gate.test.js tests/flow-durable-session-suspension.test.js && NODE_ENV=test node tests/run-quality-gates.test.js",
13
+ "test": "NODE_ENV=test node --test tests/auto-compact-prompt.test.js tests/flow-paths.test.js tests/flow-io.test.js tests/flow-config-loader.test.js tests/flow-damage-control.test.js tests/flow-output.test.js tests/flow-constants.test.js tests/flow-session-state.test.js tests/flow-hooks-integration.test.js tests/flow-utils.test.js tests/flow-security.test.js tests/flow-memory-db.test.js tests/flow-durable-session.test.js tests/flow-skill-matcher.test.js tests/flow-bridge.test.js tests/flow-proactive-compact.test.js tests/flow-cascade-completion.test.js tests/flow-capture-gate.test.js tests/flow-correction-detector-hybrid.test.js tests/flow-promote.test.js tests/flow-archive-runs.test.js tests/flow-memory.test.js tests/flow-hooks-pre-tool-helpers.test.js tests/flow-hooks-bugfix-scope-gate.test.js tests/flow-hooks-routing-gate.test.js tests/flow-hooks-phase-read-gate.test.js tests/flow-hooks-commit-log-gate.test.js tests/flow-hooks-deploy-gate.test.js tests/flow-hooks-todowrite-gate.test.js tests/flow-hooks-git-safety-gate.test.js tests/flow-hooks-scope-mutation-gate.test.js tests/flow-hooks-strike-gate.test.js tests/flow-hooks-component-check.test.js tests/flow-hooks-scope-gate.test.js tests/flow-hooks-implementation-gate.test.js tests/flow-hooks-research-gate.test.js tests/flow-hooks-loop-check.test.js tests/flow-hooks-manager-boundary-gate.test.js tests/flow-hooks-phase-gate.test.js tests/flow-hooks-pre-tool-orchestrator.test.js tests/flow-hooks-observation-capture.test.js tests/flow-hooks-task-gate.test.js tests/flow-durable-session-suspension.test.js tests/flow-health-mcp-scopes.test.js tests/flow-lean-config.test.js && NODE_ENV=test node tests/run-quality-gates.test.js",
14
14
  "test:syntax": "find scripts/ lib/ -name '*.js' -not -path '*/node_modules/*' -exec node --check {} +",
15
15
  "lint": "eslint scripts/ lib/ tests/",
16
16
  "lint:ci": "eslint scripts/ lib/ tests/ --max-warnings 0",
@@ -867,14 +867,16 @@ const CONFIG_DEFAULTS = {
867
867
  // detects the restart flag and relaunches claude. See lib/wogi-claude for
868
868
  // the wrapper. See scripts/hooks/core/task-boundary-reset.js for the trigger.
869
869
  //
870
- // REQUIRES verification against real Claude Code before enabling broadly
871
- // the SIGTERM-to-parent pattern must be confirmed to exit Claude Code
872
- // gracefully. Keep disabled until integration test in a throwaway session
873
- // confirms clean exit + flag consumption + fresh restart.
870
+ // Per-task context reset via wogi-claude wrapper. Validated in v2.17.0 for
871
+ // workspace workers (manager sessions deliberately skip restart to avoid
872
+ // orchestration storms see resolveClaudeSpawnCommand in lib/workspace.js).
873
+ // Enabled by default as of v2.19.0 after the "Sautéed worker" UX complaint:
874
+ // without it, workers sit idle after task completion instead of restarting
875
+ // fresh and ready for the next dispatch. Users who want the old behavior
876
+ // can set `enabled: false` explicitly.
874
877
  taskBoundaryReset: {
875
- _comment: 'Opt-in per-task context reset via wogi-claude wrapper. See lib/wogi-claude.',
876
- enabled: false,
877
- _comment_enabled: 'Set true ONLY after verifying SIGTERM behavior with real Claude Code in a throwaway session.',
878
+ _comment: 'Per-task context reset via wogi-claude wrapper. See lib/wogi-claude.',
879
+ enabled: true,
878
880
  maxRestartsPerSession: 50,
879
881
  _comment_maxRestartsPerSession: 'Safety cap. The wrapper also has WOGI_MAX_RESTARTS env override.'
880
882
  },
@@ -1166,6 +1168,72 @@ function mergeWithDefaults(userConfig) {
1166
1168
  return deepMerge(CONFIG_DEFAULTS, userConfig);
1167
1169
  }
1168
1170
 
1171
+ /**
1172
+ * Compute the "lean" form of a config — the minimal object that, when passed
1173
+ * through mergeWithDefaults(), reproduces the input. Removes every key whose
1174
+ * value equals the default. Nested objects are diffed recursively; arrays and
1175
+ * primitives are compared by JSON equality.
1176
+ *
1177
+ * Always preserves these identity keys even if they match defaults:
1178
+ * - `$schema`, `version`, `projectName`, `cli`, `_configVersion`
1179
+ * These anchor the file's purpose and let tooling (VS Code JSON schema, config
1180
+ * migrations) work correctly on the lean file.
1181
+ *
1182
+ * Round-trip guarantee:
1183
+ * mergeWithDefaults(computeLeanConfig(full)) deep-equals mergeWithDefaults(full)
1184
+ * for any input — verified in tests.
1185
+ *
1186
+ * @param {Object} fullConfig - A fully-merged config (or any config object)
1187
+ * @returns {Object} Lean config containing only overrides + identity keys
1188
+ */
1189
+ function computeLeanConfig(fullConfig) {
1190
+ if (!fullConfig || typeof fullConfig !== 'object') {
1191
+ return {};
1192
+ }
1193
+ const IDENTITY_KEYS = new Set(['$schema', 'version', 'projectName', 'cli', '_configVersion', 'projectType']);
1194
+ return diffAgainstDefaults(fullConfig, CONFIG_DEFAULTS, IDENTITY_KEYS, true);
1195
+ }
1196
+
1197
+ function diffAgainstDefaults(user, defaults, identityKeys, isRoot) {
1198
+ const out = {};
1199
+ for (const key of Object.keys(user)) {
1200
+ const uVal = user[key];
1201
+ const dVal = defaults ? defaults[key] : undefined;
1202
+
1203
+ // Preserve identity keys at the root regardless of equality.
1204
+ if (isRoot && identityKeys.has(key)) {
1205
+ out[key] = uVal;
1206
+ continue;
1207
+ }
1208
+
1209
+ // Comment fields (prefix _comment) are metadata from the defaults file —
1210
+ // don't propagate into user configs, they bloat without adding meaning.
1211
+ if (typeof key === 'string' && key.startsWith('_comment')) {
1212
+ continue;
1213
+ }
1214
+
1215
+ if (dVal === undefined) {
1216
+ // Key doesn't exist in defaults — it's fully user-defined, keep it.
1217
+ out[key] = uVal;
1218
+ continue;
1219
+ }
1220
+
1221
+ if (isPlainObject(uVal) && isPlainObject(dVal)) {
1222
+ const nested = diffAgainstDefaults(uVal, dVal, identityKeys, false);
1223
+ if (Object.keys(nested).length > 0) {
1224
+ out[key] = nested;
1225
+ }
1226
+ continue;
1227
+ }
1228
+
1229
+ // Primitive / array / null comparison via JSON stringify.
1230
+ if (JSON.stringify(uVal) !== JSON.stringify(dVal)) {
1231
+ out[key] = uVal;
1232
+ }
1233
+ }
1234
+ return out;
1235
+ }
1236
+
1169
1237
  /**
1170
1238
  * Apply project-type-aware defaults to a config.
1171
1239
  * Strips/disables irrelevant sections based on detected project type.
@@ -1249,5 +1317,6 @@ module.exports = {
1249
1317
  isPlainObject,
1250
1318
  getDefaultsForKey,
1251
1319
  mergeWithDefaults,
1320
+ computeLeanConfig,
1252
1321
  applyProjectTypeDefaults
1253
1322
  };