wogiflow 2.18.0 → 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 +11 -0
- package/lib/commands/config.js +148 -0
- package/lib/installer.js +62 -8
- package/package.json +2 -2
- package/scripts/flow-config-defaults.js +76 -7
- package/scripts/hooks/core/task-gate.js +92 -7
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
|
|
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
|
|
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
|
-
|
|
532
|
-
|
|
533
|
-
//
|
|
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(
|
|
537
|
-
} catch (
|
|
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
|
-
|
|
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.
|
|
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
|
-
//
|
|
871
|
-
//
|
|
872
|
-
//
|
|
873
|
-
//
|
|
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: '
|
|
876
|
-
enabled:
|
|
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
|
};
|
|
@@ -418,19 +418,104 @@ function generateWarningMessage(operation, filePath) {
|
|
|
418
418
|
return `Warning: ${operation === 'write' ? 'Creating' : 'Editing'} ${fileName} without an active task. Consider starting a task first.`;
|
|
419
419
|
}
|
|
420
420
|
|
|
421
|
+
// Rule / memory files where /wogi-decide is the idiomatic capture command.
|
|
422
|
+
// Intent artifacts (domain-model.md, user-journeys.md, glossary.md, product.md)
|
|
423
|
+
// are deliberately excluded — modifying those is product-design work and should
|
|
424
|
+
// flow through /wogi-story per the normal task lifecycle.
|
|
425
|
+
const RULE_FILE_BASENAMES = new Set([
|
|
426
|
+
'decisions.md',
|
|
427
|
+
'feedback-patterns.md',
|
|
428
|
+
'MEMORY.md'
|
|
429
|
+
]);
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Detect whether the given file path (or its basename) is a rule/memory file
|
|
433
|
+
* that should route to /wogi-decide for capture instead of /wogi-story.
|
|
434
|
+
*
|
|
435
|
+
* Also matches Claude Code auto-memory paths under `.claude/projects/*\/memory/`.
|
|
436
|
+
*/
|
|
437
|
+
function isRuleOrMemoryFile(filePath) {
|
|
438
|
+
if (!filePath || typeof filePath !== 'string') return false;
|
|
439
|
+
const base = path.basename(filePath);
|
|
440
|
+
if (RULE_FILE_BASENAMES.has(base)) return true;
|
|
441
|
+
if (filePath.includes(`${path.sep}.claude${path.sep}projects${path.sep}`) &&
|
|
442
|
+
filePath.includes(`${path.sep}memory${path.sep}`)) return true;
|
|
443
|
+
return false;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Detect whether the current working directory (or its parents) contains a
|
|
448
|
+
* `.workspace/` directory — the marker for WogiFlow workspace mode where a
|
|
449
|
+
* manager repo coordinates worker repos. When present, the block message
|
|
450
|
+
* additionally suggests the workspace-coordination task pattern.
|
|
451
|
+
*
|
|
452
|
+
* Walks up at most 6 parents to avoid unbounded traversal.
|
|
453
|
+
*/
|
|
454
|
+
function isInWorkspaceMode(startDir) {
|
|
455
|
+
const start = startDir || process.cwd();
|
|
456
|
+
try {
|
|
457
|
+
let dir = start;
|
|
458
|
+
for (let i = 0; i < 6; i++) {
|
|
459
|
+
const candidate = path.join(dir, '.workspace');
|
|
460
|
+
if (fs.existsSync(candidate) && fs.statSync(candidate).isDirectory()) {
|
|
461
|
+
return true;
|
|
462
|
+
}
|
|
463
|
+
const parent = path.dirname(dir);
|
|
464
|
+
if (parent === dir) break;
|
|
465
|
+
dir = parent;
|
|
466
|
+
}
|
|
467
|
+
} catch (_err) {
|
|
468
|
+
// Filesystem errors (permissions, etc.) → conservatively say no. The
|
|
469
|
+
// standard suggestions are still shown, so the user is never stranded.
|
|
470
|
+
}
|
|
471
|
+
return false;
|
|
472
|
+
}
|
|
473
|
+
|
|
421
474
|
/**
|
|
422
|
-
* Generate block message
|
|
475
|
+
* Generate block message (context-aware).
|
|
476
|
+
*
|
|
477
|
+
* The message adapts to two signals:
|
|
478
|
+
* 1. If the blocked path is a rule/memory file (e.g. decisions.md), the
|
|
479
|
+
* message leads with `/wogi-decide` — the idiomatic "from now on" rule
|
|
480
|
+
* capture command that doesn't require a code task.
|
|
481
|
+
* 2. If a `.workspace/` directory exists at cwd or a parent, the message
|
|
482
|
+
* adds a workspace-coordination task pattern.
|
|
483
|
+
*
|
|
484
|
+
* Enforcement behavior is unchanged — this only affects the text shown to
|
|
485
|
+
* the AI / user when the gate blocks. Intent artifacts (domain-model.md,
|
|
486
|
+
* user-journeys.md, glossary.md, product.md) deliberately do NOT trigger
|
|
487
|
+
* the rule-file branch — those still route to /wogi-story.
|
|
423
488
|
*/
|
|
424
489
|
function generateBlockMessage(operation, filePath) {
|
|
425
490
|
const fileName = filePath ? path.basename(filePath) : 'file';
|
|
426
|
-
|
|
491
|
+
const isRuleFile = isRuleOrMemoryFile(filePath);
|
|
492
|
+
const inWorkspace = isInWorkspaceMode();
|
|
493
|
+
|
|
494
|
+
const lines = [`Cannot ${operation} ${fileName} without an active task.`, ''];
|
|
427
495
|
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
496
|
+
if (isRuleFile) {
|
|
497
|
+
lines.push('For rule / memory capture without a code task:');
|
|
498
|
+
lines.push(' /wogi-decide "from now on, <your rule>"');
|
|
499
|
+
lines.push('');
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
if (inWorkspace) {
|
|
503
|
+
lines.push('For workspace coordination (manager repo in workspace mode):');
|
|
504
|
+
lines.push(' /wogi-start "coordinate wf-XXXXXXXX in workspace"');
|
|
505
|
+
lines.push('');
|
|
506
|
+
}
|
|
432
507
|
|
|
433
|
-
|
|
508
|
+
lines.push('For a side thought or idea to save for later:');
|
|
509
|
+
lines.push(' /wogi-capture "your idea"');
|
|
510
|
+
lines.push('');
|
|
511
|
+
lines.push('Other options:');
|
|
512
|
+
lines.push(' /wogi-ready → see available tasks');
|
|
513
|
+
lines.push(' /wogi-start wf-XXXXXXXX → start an existing task');
|
|
514
|
+
lines.push(' /wogi-story "description" → create a new task');
|
|
515
|
+
lines.push('');
|
|
516
|
+
lines.push('Task gating is enforced when strictMode is enabled.');
|
|
517
|
+
|
|
518
|
+
return lines.join('\n');
|
|
434
519
|
}
|
|
435
520
|
|
|
436
521
|
module.exports = {
|