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.
- package/.claude/commands/configure-vcs.md +102 -102
- package/.claude/commands/forge.md +218 -218
- package/.claude/hooks/worker-loop.js +220 -217
- package/.claude/settings.json +89 -89
- package/README.md +149 -191
- package/agents/aegis/personality.md +303 -303
- package/agents/anvil/personality.md +278 -278
- package/agents/architect/personality.md +260 -260
- package/agents/crucible/personality.md +362 -362
- package/agents/crucible-x/personality.md +210 -210
- package/agents/ember/personality.md +293 -293
- package/agents/flux/personality.md +248 -248
- package/agents/furnace/personality.md +342 -342
- package/agents/herald/personality.md +249 -249
- package/agents/oracle/personality.md +284 -284
- package/agents/pixel/personality.md +140 -140
- package/agents/planning-hub/personality.md +473 -473
- package/agents/scribe/personality.md +253 -253
- package/agents/slag/personality.md +268 -268
- package/agents/temper/personality.md +270 -270
- package/bin/cli.js +372 -372
- package/bin/forge-daemon.sh +477 -477
- package/bin/forge-setup.sh +662 -661
- package/bin/forge-spawn.sh +164 -164
- package/bin/forge.sh +566 -566
- package/docs/commands.md +8 -8
- package/package.json +77 -77
- package/{bin → src}/lib/agents.sh +177 -177
- package/{bin → src}/lib/check-aliases.js +50 -50
- package/{bin → src}/lib/colors.sh +45 -44
- package/{bin → src}/lib/config.sh +347 -347
- package/{bin → src}/lib/constants.sh +241 -241
- package/{bin → src}/lib/daemon/budgets.sh +107 -107
- package/{bin → src}/lib/daemon/dependencies.sh +146 -146
- package/{bin → src}/lib/daemon/display.sh +128 -128
- package/{bin → src}/lib/daemon/notifications.sh +273 -273
- package/{bin → src}/lib/daemon/routing.sh +93 -93
- package/{bin → src}/lib/daemon/state.sh +163 -163
- package/{bin → src}/lib/daemon/sync.sh +103 -103
- package/{bin → src}/lib/database.sh +357 -357
- package/{bin → src}/lib/frontmatter.js +106 -106
- package/{bin → src}/lib/heimdall-setup.js +113 -113
- package/{bin → src}/lib/heimdall.js +265 -265
- package/src/lib/index.sh +25 -0
- package/{bin → src}/lib/json.sh +264 -264
- package/{bin → src}/lib/terminal.js +452 -452
- package/{bin → src}/lib/util.sh +126 -126
- package/{bin → src}/lib/vcs.js +349 -349
- package/{context → templates}/project-context-template.md +122 -122
- package/config/task-template.md +0 -159
- 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 }
|