vibe-forge 0.8.1 → 0.8.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.
Files changed (51) hide show
  1. package/.claude/commands/configure-vcs.md +102 -102
  2. package/.claude/commands/forge.md +218 -218
  3. package/.claude/hooks/worker-loop.js +220 -217
  4. package/.claude/settings.json +89 -89
  5. package/README.md +149 -191
  6. package/agents/aegis/personality.md +303 -303
  7. package/agents/anvil/personality.md +278 -278
  8. package/agents/architect/personality.md +260 -260
  9. package/agents/crucible/personality.md +362 -362
  10. package/agents/crucible-x/personality.md +210 -210
  11. package/agents/ember/personality.md +293 -293
  12. package/agents/flux/personality.md +248 -248
  13. package/agents/furnace/personality.md +342 -342
  14. package/agents/herald/personality.md +249 -249
  15. package/agents/oracle/personality.md +284 -284
  16. package/agents/pixel/personality.md +140 -140
  17. package/agents/planning-hub/personality.md +473 -473
  18. package/agents/scribe/personality.md +253 -253
  19. package/agents/slag/personality.md +268 -268
  20. package/agents/temper/personality.md +270 -270
  21. package/bin/cli.js +372 -372
  22. package/bin/forge-daemon.sh +477 -477
  23. package/bin/forge-setup.sh +662 -661
  24. package/bin/forge-spawn.sh +164 -164
  25. package/bin/forge.sh +566 -566
  26. package/docs/commands.md +8 -8
  27. package/package.json +77 -77
  28. package/{bin → src}/lib/agents.sh +177 -177
  29. package/{bin → src}/lib/check-aliases.js +50 -50
  30. package/{bin → src}/lib/colors.sh +45 -44
  31. package/{bin → src}/lib/config.sh +347 -347
  32. package/{bin → src}/lib/constants.sh +241 -241
  33. package/{bin → src}/lib/daemon/budgets.sh +107 -107
  34. package/{bin → src}/lib/daemon/dependencies.sh +146 -146
  35. package/{bin → src}/lib/daemon/display.sh +128 -128
  36. package/{bin → src}/lib/daemon/notifications.sh +273 -273
  37. package/{bin → src}/lib/daemon/routing.sh +93 -93
  38. package/{bin → src}/lib/daemon/state.sh +163 -163
  39. package/{bin → src}/lib/daemon/sync.sh +103 -103
  40. package/{bin → src}/lib/database.sh +357 -357
  41. package/{bin → src}/lib/frontmatter.js +106 -106
  42. package/{bin → src}/lib/heimdall-setup.js +113 -113
  43. package/{bin → src}/lib/heimdall.js +265 -265
  44. package/src/lib/index.sh +25 -0
  45. package/{bin → src}/lib/json.sh +264 -264
  46. package/{bin → src}/lib/terminal.js +452 -452
  47. package/{bin → src}/lib/util.sh +126 -126
  48. package/{bin → src}/lib/vcs.js +349 -349
  49. package/{context → templates}/project-context-template.md +122 -122
  50. package/config/task-template.md +0 -159
  51. package/config/templates/handoff-template.md +0 -40
@@ -1,106 +1,106 @@
1
- #!/usr/bin/env node
2
- /**
3
- * frontmatter.js - Safe YAML frontmatter field extractor
4
- *
5
- * Replaces grep/cut pipelines for extracting frontmatter fields from
6
- * task and attention markdown files (RT-20260405-001 MEDIUM-5).
7
- *
8
- * Usage:
9
- * node frontmatter.js <file> <field1> [field2] ...
10
- * node frontmatter.js --section <file> <heading>
11
- *
12
- * Output (field mode):
13
- * field1=value1
14
- * field2=value2
15
- *
16
- * Output (section mode):
17
- * First non-heading line under the matched ## heading
18
- *
19
- * Values are sanitized for safe shell consumption:
20
- * - Shell metacharacters removed
21
- * - Length capped at 200 chars
22
- * - Missing fields output as empty: field=
23
- */
24
-
25
- 'use strict';
26
-
27
- const fs = require('fs');
28
- const yaml = require('js-yaml');
29
-
30
- const MAX_VALUE_LENGTH = 200;
31
-
32
- // Strip characters unsafe for shell interpolation
33
- function sanitize(val) {
34
- if (val == null) return '';
35
- const str = String(val)
36
- .replace(/[\0\r]/g, '')
37
- .replace(/[\n]/g, ' ')
38
- .replace(/[$`"'\\(){}[\]!#;|&<>]/g, '')
39
- .trim();
40
- return str.substring(0, MAX_VALUE_LENGTH);
41
- }
42
-
43
- function extractFrontmatter(content) {
44
- const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
45
- if (!match) return {};
46
- try {
47
- const parsed = yaml.load(match[1]);
48
- return typeof parsed === 'object' && parsed !== null ? parsed : {};
49
- } catch (_) {
50
- return {};
51
- }
52
- }
53
-
54
- function extractSection(content, heading) {
55
- const pattern = new RegExp(`^## ${heading.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`, 'im');
56
- const idx = content.search(pattern);
57
- if (idx === -1) return '';
58
- const after = content.substring(idx).split('\n').slice(1);
59
- for (const line of after) {
60
- if (line.startsWith('## ')) break;
61
- const trimmed = line.trim();
62
- if (trimmed) return sanitize(trimmed);
63
- }
64
- return '';
65
- }
66
-
67
- // Main
68
- const args = process.argv.slice(2);
69
-
70
- if (args.length < 2) {
71
- process.stderr.write('Usage: node frontmatter.js <file> <field1> [field2] ...\n');
72
- process.stderr.write(' node frontmatter.js --section <file> <heading>\n');
73
- process.exit(1);
74
- }
75
-
76
- // Section mode: --section <file> <heading>
77
- if (args[0] === '--section') {
78
- const file = args[1];
79
- const heading = args[2];
80
- if (!file || !heading) {
81
- process.stderr.write('Usage: node frontmatter.js --section <file> <heading>\n');
82
- process.exit(1);
83
- }
84
- let content;
85
- try { content = fs.readFileSync(file, 'utf8'); } catch (_) { process.exit(0); }
86
- process.stdout.write(extractSection(content, heading) + '\n');
87
- process.exit(0);
88
- }
89
-
90
- // Field mode: <file> <field1> [field2] ...
91
- const file = args[0];
92
- const fields = args.slice(1);
93
-
94
- let content;
95
- try { content = fs.readFileSync(file, 'utf8'); } catch (_) {
96
- // File unreadable: output empty values
97
- for (const f of fields) process.stdout.write(`${f}=\n`);
98
- process.exit(0);
99
- }
100
-
101
- const data = extractFrontmatter(content);
102
-
103
- for (const field of fields) {
104
- const val = sanitize(data[field]);
105
- process.stdout.write(`${field}=${val}\n`);
106
- }
1
+ #!/usr/bin/env node
2
+ /**
3
+ * frontmatter.js - Safe YAML frontmatter field extractor
4
+ *
5
+ * Replaces grep/cut pipelines for extracting frontmatter fields from
6
+ * task and attention markdown files (RT-20260405-001 MEDIUM-5).
7
+ *
8
+ * Usage:
9
+ * node frontmatter.js <file> <field1> [field2] ...
10
+ * node frontmatter.js --section <file> <heading>
11
+ *
12
+ * Output (field mode):
13
+ * field1=value1
14
+ * field2=value2
15
+ *
16
+ * Output (section mode):
17
+ * First non-heading line under the matched ## heading
18
+ *
19
+ * Values are sanitized for safe shell consumption:
20
+ * - Shell metacharacters removed
21
+ * - Length capped at 200 chars
22
+ * - Missing fields output as empty: field=
23
+ */
24
+
25
+ 'use strict';
26
+
27
+ const fs = require('fs');
28
+ const yaml = require('js-yaml');
29
+
30
+ const MAX_VALUE_LENGTH = 200;
31
+
32
+ // Strip characters unsafe for shell interpolation
33
+ function sanitize(val) {
34
+ if (val == null) return '';
35
+ const str = String(val)
36
+ .replace(/[\0\r]/g, '')
37
+ .replace(/[\n]/g, ' ')
38
+ .replace(/[$`"'\\(){}[\]!#;|&<>]/g, '')
39
+ .trim();
40
+ return str.substring(0, MAX_VALUE_LENGTH);
41
+ }
42
+
43
+ function extractFrontmatter(content) {
44
+ const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
45
+ if (!match) return {};
46
+ try {
47
+ const parsed = yaml.load(match[1]);
48
+ return typeof parsed === 'object' && parsed !== null ? parsed : {};
49
+ } catch (_) {
50
+ return {};
51
+ }
52
+ }
53
+
54
+ function extractSection(content, heading) {
55
+ const pattern = new RegExp(`^## ${heading.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`, 'im');
56
+ const idx = content.search(pattern);
57
+ if (idx === -1) return '';
58
+ const after = content.substring(idx).split('\n').slice(1);
59
+ for (const line of after) {
60
+ if (line.startsWith('## ')) break;
61
+ const trimmed = line.trim();
62
+ if (trimmed) return sanitize(trimmed);
63
+ }
64
+ return '';
65
+ }
66
+
67
+ // Main
68
+ const args = process.argv.slice(2);
69
+
70
+ if (args.length < 2) {
71
+ process.stderr.write('Usage: node frontmatter.js <file> <field1> [field2] ...\n');
72
+ process.stderr.write(' node frontmatter.js --section <file> <heading>\n');
73
+ process.exit(1);
74
+ }
75
+
76
+ // Section mode: --section <file> <heading>
77
+ if (args[0] === '--section') {
78
+ const file = args[1];
79
+ const heading = args[2];
80
+ if (!file || !heading) {
81
+ process.stderr.write('Usage: node frontmatter.js --section <file> <heading>\n');
82
+ process.exit(1);
83
+ }
84
+ let content;
85
+ try { content = fs.readFileSync(file, 'utf8'); } catch (_) { process.exit(0); }
86
+ process.stdout.write(extractSection(content, heading) + '\n');
87
+ process.exit(0);
88
+ }
89
+
90
+ // Field mode: <file> <field1> [field2] ...
91
+ const file = args[0];
92
+ const fields = args.slice(1);
93
+
94
+ let content;
95
+ try { content = fs.readFileSync(file, 'utf8'); } catch (_) {
96
+ // File unreadable: output empty values
97
+ for (const f of fields) process.stdout.write(`${f}=\n`);
98
+ process.exit(0);
99
+ }
100
+
101
+ const data = extractFrontmatter(content);
102
+
103
+ for (const field of fields) {
104
+ const val = sanitize(data[field]);
105
+ process.stdout.write(`${field}=${val}\n`);
106
+ }
@@ -1,113 +1,113 @@
1
- #!/usr/bin/env node
2
- /**
3
- * Heimdall Setup -- writes .claude/settings.local.json into a worker's
4
- * working directory to register Heimdall as a PreToolUse hook.
5
- *
6
- * Called by the forge daemon at inbox-write time, before the worker
7
- * picks up a lab task.
8
- *
9
- * Uses a merge strategy: if settings.local.json already exists, the
10
- * Heimdall hooks are merged into the existing PreToolUse array rather
11
- * than overwriting the file.
12
- */
13
-
14
- 'use strict'
15
-
16
- const fs = require('fs')
17
- const path = require('path')
18
-
19
- // Absolute path to heimdall.js -- resolvable from any working directory
20
- const HEIMDALL_PATH = path.resolve(__dirname, 'heimdall.js').replace(/\\/g, '/')
21
-
22
- const HEIMDALL_HOOKS = ['Bash', 'Write', 'Edit'].map(matcher => ({
23
- matcher,
24
- hooks: [{ type: 'command', command: `node "${HEIMDALL_PATH}"` }],
25
- }))
26
-
27
- /**
28
- * writeHeimdallHooks(worktreePath)
29
- *
30
- * Writes or merges Heimdall PreToolUse hooks into:
31
- * <worktreePath>/.claude/settings.local.json
32
- *
33
- * Safe to call multiple times -- idempotent.
34
- *
35
- * @param {string} worktreePath Absolute path to the worker's worktree root
36
- */
37
- function writeHeimdallHooks(worktreePath) {
38
- const claudeDir = path.join(worktreePath, '.claude')
39
- const settingsPath = path.join(claudeDir, 'settings.local.json')
40
-
41
- // Ensure .claude/ exists
42
- if (!fs.existsSync(claudeDir)) {
43
- fs.mkdirSync(claudeDir, { recursive: true })
44
- }
45
-
46
- // Read existing settings if present
47
- let existing = {}
48
- if (fs.existsSync(settingsPath)) {
49
- try {
50
- existing = JSON.parse(fs.readFileSync(settingsPath, 'utf8'))
51
- } catch (_) {
52
- // Corrupt file -- start fresh
53
- existing = {}
54
- }
55
- }
56
-
57
- // Ensure hooks structure exists
58
- if (!existing.hooks) existing.hooks = {}
59
- if (!existing.hooks.PreToolUse) existing.hooks.PreToolUse = []
60
-
61
- // Merge: add Heimdall hook entries for matchers not already registered
62
- const existingMatchers = new Set(
63
- existing.hooks.PreToolUse.map(h => h.matcher)
64
- )
65
-
66
- for (const heimdallHook of HEIMDALL_HOOKS) {
67
- if (!existingMatchers.has(heimdallHook.matcher)) {
68
- existing.hooks.PreToolUse.push(heimdallHook)
69
- }
70
- }
71
-
72
- fs.writeFileSync(settingsPath, JSON.stringify(existing, null, 2) + '\n')
73
- }
74
-
75
- /**
76
- * writeContextFile(worktreePath, context)
77
- *
78
- * Writes the Heimdall context file to the worktree root so Heimdall
79
- * can read per-task policy on every invocation.
80
- *
81
- * @param {string} worktreePath Absolute path to the worker's worktree root
82
- * @param {object} context Context object matching the schema below
83
- *
84
- * Context schema:
85
- * {
86
- * story_id: string -- lab story ID (e.g. "FORGE-3")
87
- * agent: string -- worker name (e.g. "anvil")
88
- * worktree_path: string -- absolute path to worktree (same as worktreePath)
89
- * assigned_branch: string -- git branch for this story
90
- * handoff_dir: string -- absolute path to _vibe-chain-output/handoffs/
91
- * escalation_dir: string -- absolute path to worker-inbox/<agent>/ dir
92
- * audit_log: string -- absolute path to heimdall-audit.log
93
- * has_db_migration: boolean
94
- * has_api_changes: boolean
95
- * allowed_paths: string[] -- absolute paths the worker may read/write
96
- * }
97
- */
98
- function writeContextFile(worktreePath, context) {
99
- const contextPath = path.join(worktreePath, '.context.json')
100
- fs.writeFileSync(contextPath, JSON.stringify(context, null, 2) + '\n')
101
- }
102
-
103
- /**
104
- * setup(worktreePath, context)
105
- *
106
- * Convenience function: writes both hooks and context file in one call.
107
- */
108
- function setup(worktreePath, context) {
109
- writeHeimdallHooks(worktreePath)
110
- writeContextFile(worktreePath, context)
111
- }
112
-
113
- module.exports = { setup, writeHeimdallHooks, writeContextFile, HEIMDALL_PATH }
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Heimdall Setup -- writes .claude/settings.local.json into a worker's
4
+ * working directory to register Heimdall as a PreToolUse hook.
5
+ *
6
+ * Called by the forge daemon at inbox-write time, before the worker
7
+ * picks up a lab task.
8
+ *
9
+ * Uses a merge strategy: if settings.local.json already exists, the
10
+ * Heimdall hooks are merged into the existing PreToolUse array rather
11
+ * than overwriting the file.
12
+ */
13
+
14
+ 'use strict'
15
+
16
+ const fs = require('fs')
17
+ const path = require('path')
18
+
19
+ // Absolute path to heimdall.js -- resolvable from any working directory
20
+ const HEIMDALL_PATH = path.resolve(__dirname, 'heimdall.js').replace(/\\/g, '/')
21
+
22
+ const HEIMDALL_HOOKS = ['Bash', 'Write', 'Edit'].map(matcher => ({
23
+ matcher,
24
+ hooks: [{ type: 'command', command: `node "${HEIMDALL_PATH}"` }],
25
+ }))
26
+
27
+ /**
28
+ * writeHeimdallHooks(worktreePath)
29
+ *
30
+ * Writes or merges Heimdall PreToolUse hooks into:
31
+ * <worktreePath>/.claude/settings.local.json
32
+ *
33
+ * Safe to call multiple times -- idempotent.
34
+ *
35
+ * @param {string} worktreePath Absolute path to the worker's worktree root
36
+ */
37
+ function writeHeimdallHooks(worktreePath) {
38
+ const claudeDir = path.join(worktreePath, '.claude')
39
+ const settingsPath = path.join(claudeDir, 'settings.local.json')
40
+
41
+ // Ensure .claude/ exists
42
+ if (!fs.existsSync(claudeDir)) {
43
+ fs.mkdirSync(claudeDir, { recursive: true })
44
+ }
45
+
46
+ // Read existing settings if present
47
+ let existing = {}
48
+ if (fs.existsSync(settingsPath)) {
49
+ try {
50
+ existing = JSON.parse(fs.readFileSync(settingsPath, 'utf8'))
51
+ } catch (_) {
52
+ // Corrupt file -- start fresh
53
+ existing = {}
54
+ }
55
+ }
56
+
57
+ // Ensure hooks structure exists
58
+ if (!existing.hooks) existing.hooks = {}
59
+ if (!existing.hooks.PreToolUse) existing.hooks.PreToolUse = []
60
+
61
+ // Merge: add Heimdall hook entries for matchers not already registered
62
+ const existingMatchers = new Set(
63
+ existing.hooks.PreToolUse.map(h => h.matcher)
64
+ )
65
+
66
+ for (const heimdallHook of HEIMDALL_HOOKS) {
67
+ if (!existingMatchers.has(heimdallHook.matcher)) {
68
+ existing.hooks.PreToolUse.push(heimdallHook)
69
+ }
70
+ }
71
+
72
+ fs.writeFileSync(settingsPath, JSON.stringify(existing, null, 2) + '\n')
73
+ }
74
+
75
+ /**
76
+ * writeContextFile(worktreePath, context)
77
+ *
78
+ * Writes the Heimdall context file to the worktree root so Heimdall
79
+ * can read per-task policy on every invocation.
80
+ *
81
+ * @param {string} worktreePath Absolute path to the worker's worktree root
82
+ * @param {object} context Context object matching the schema below
83
+ *
84
+ * Context schema:
85
+ * {
86
+ * story_id: string -- lab story ID (e.g. "FORGE-3")
87
+ * agent: string -- worker name (e.g. "anvil")
88
+ * worktree_path: string -- absolute path to worktree (same as worktreePath)
89
+ * assigned_branch: string -- git branch for this story
90
+ * handoff_dir: string -- absolute path to _vibe-chain-output/handoffs/
91
+ * escalation_dir: string -- absolute path to worker-inbox/<agent>/ dir
92
+ * audit_log: string -- absolute path to heimdall-audit.log
93
+ * has_db_migration: boolean
94
+ * has_api_changes: boolean
95
+ * allowed_paths: string[] -- absolute paths the worker may read/write
96
+ * }
97
+ */
98
+ function writeContextFile(worktreePath, context) {
99
+ const contextPath = path.join(worktreePath, '.context.json')
100
+ fs.writeFileSync(contextPath, JSON.stringify(context, null, 2) + '\n')
101
+ }
102
+
103
+ /**
104
+ * setup(worktreePath, context)
105
+ *
106
+ * Convenience function: writes both hooks and context file in one call.
107
+ */
108
+ function setup(worktreePath, context) {
109
+ writeHeimdallHooks(worktreePath)
110
+ writeContextFile(worktreePath, context)
111
+ }
112
+
113
+ module.exports = { setup, writeHeimdallHooks, writeContextFile, HEIMDALL_PATH }