brainclaw 1.5.5 → 1.6.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/dist/brainclaw-vscode.vsix +0 -0
- package/dist/cli.js +124 -7
- package/dist/commands/bootstrap-loop.js +206 -0
- package/dist/commands/loop.js +156 -0
- package/dist/commands/loops-handlers.js +110 -55
- package/dist/commands/mcp-read-handlers.js +37 -0
- package/dist/commands/mcp.js +621 -202
- package/dist/commands/questions.js +180 -0
- package/dist/commands/reply.js +190 -0
- package/dist/commands/session-end.js +105 -3
- package/dist/commands/session-start.js +32 -53
- package/dist/commands/switch.js +17 -1
- package/dist/core/agentrun-reconciler.js +65 -0
- package/dist/core/claims.js +29 -0
- package/dist/core/dispatch-status.js +219 -0
- package/dist/core/entity-operations.js +128 -9
- package/dist/core/execution-adapters.js +38 -2
- package/dist/core/facade-schema.js +55 -0
- package/dist/core/federation-cloud.js +27 -12
- package/dist/core/federation-materialize.js +57 -0
- package/dist/core/instruction-templates.js +2 -0
- package/dist/core/loops/bootstrap-acquire.js +195 -0
- package/dist/core/loops/facade-schema.js +68 -1
- package/dist/core/loops/hooks/bootstrap-write.js +144 -0
- package/dist/core/loops/hooks/notify-operator.js +148 -0
- package/dist/core/loops/hooks/survey-source-reader.js +256 -0
- package/dist/core/loops/index.js +8 -2
- package/dist/core/loops/next-expected.js +63 -0
- package/dist/core/loops/presets/bootstrap.js +75 -0
- package/dist/core/loops/presets/index.js +16 -0
- package/dist/core/loops/store.js +224 -4
- package/dist/core/loops/types.js +346 -1
- package/dist/core/loops/verbs.js +739 -6
- package/dist/core/schema.js +28 -2
- package/dist/core/state.js +62 -0
- package/dist/facts.js +7 -5
- package/dist/facts.json +6 -4
- package/docs/concepts/dispatch-lifecycle.md +228 -0
- package/docs/concepts/loop-engine.md +55 -0
- package/docs/concepts/multi-agent-workflows.md +167 -166
- package/docs/concepts/troubleshooting.md +10 -2
- package/docs/integrations/overview.md +14 -12
- package/package.json +1 -1
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import child_process from 'node:child_process';
|
|
2
|
+
/**
|
|
3
|
+
* pln#513 step 4 — OS notifications hook on input_requested events.
|
|
4
|
+
*
|
|
5
|
+
* Best-effort, fire-and-forget OS-native heads-up so the operator notices
|
|
6
|
+
* when a bootstrap loop pauses on an operator_question. Gated by
|
|
7
|
+
* `BRAINCLAW_OPERATOR_NOTIFICATIONS=1` (opt-in) and scoped to bootstrap-preset
|
|
8
|
+
* loops in v1. Every code path is wrapped so a missing notifier binary,
|
|
9
|
+
* unparseable artifact body, or spawn error never propagates to the caller —
|
|
10
|
+
* the journal write must remain the source of truth.
|
|
11
|
+
*/
|
|
12
|
+
const TITLE = 'brainclaw';
|
|
13
|
+
const QUESTION_TEXT_CAP = 80;
|
|
14
|
+
function isEnabled() {
|
|
15
|
+
return process.env.BRAINCLAW_OPERATOR_NOTIFICATIONS === '1';
|
|
16
|
+
}
|
|
17
|
+
function isBootstrapLoop(loop) {
|
|
18
|
+
return loop.protocol?.preset === 'bootstrap';
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Resolve the matching operator_question artifact body for the event's
|
|
22
|
+
* question_id and return its question_text, truncated. Returns undefined
|
|
23
|
+
* whenever the artifact can't be located or its body fails to parse —
|
|
24
|
+
* the notification body still works without it.
|
|
25
|
+
*/
|
|
26
|
+
function resolveQuestionText(event, loop) {
|
|
27
|
+
if (event.kind !== 'input_requested')
|
|
28
|
+
return undefined;
|
|
29
|
+
for (const artifact of loop.artifacts) {
|
|
30
|
+
if (artifact.type !== 'operator_question' || artifact.body === undefined)
|
|
31
|
+
continue;
|
|
32
|
+
try {
|
|
33
|
+
const body = JSON.parse(artifact.body);
|
|
34
|
+
if (body.question_id === event.question_id) {
|
|
35
|
+
const text = body.question_text;
|
|
36
|
+
if (typeof text !== 'string' || text.length === 0)
|
|
37
|
+
return undefined;
|
|
38
|
+
return text.length > QUESTION_TEXT_CAP
|
|
39
|
+
? `${text.slice(0, QUESTION_TEXT_CAP)}…`
|
|
40
|
+
: text;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
// ignore unparseable bodies; fall through to the next artifact
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Sanitize the message before passing it to a shell-bridge command
|
|
51
|
+
* (osascript, powershell). We allow only printable ASCII apart from
|
|
52
|
+
* double-quotes / backticks / backslashes / control chars to avoid quoting
|
|
53
|
+
* pitfalls on every platform. The fallback `notify-send` on Linux runs
|
|
54
|
+
* via an arg vector so its sanitization is just length-capping.
|
|
55
|
+
*/
|
|
56
|
+
function sanitizeForShell(message) {
|
|
57
|
+
return message
|
|
58
|
+
.replace(/["`\\$]/g, '')
|
|
59
|
+
.replace(/[\r\n\t]/g, ' ')
|
|
60
|
+
.replace(/[\x00-\x1f\x7f]/g, '');
|
|
61
|
+
}
|
|
62
|
+
function composeMessage(event, loop) {
|
|
63
|
+
const base = `brainclaw bootstrap: question awaiting input on loop ${loop.id}`;
|
|
64
|
+
const text = resolveQuestionText(event, loop);
|
|
65
|
+
return text ? `${base} — ${text}` : base;
|
|
66
|
+
}
|
|
67
|
+
function spawnDetached(command, args) {
|
|
68
|
+
const child = child_process.spawn(command, args, {
|
|
69
|
+
detached: true,
|
|
70
|
+
stdio: 'ignore',
|
|
71
|
+
windowsHide: true,
|
|
72
|
+
});
|
|
73
|
+
child.on('error', () => {
|
|
74
|
+
// missing binary or exec failure — best-effort, swallow.
|
|
75
|
+
});
|
|
76
|
+
child.unref();
|
|
77
|
+
}
|
|
78
|
+
function notifyLinux(message) {
|
|
79
|
+
spawnDetached('notify-send', [TITLE, message]);
|
|
80
|
+
}
|
|
81
|
+
function notifyMac(message) {
|
|
82
|
+
const safe = sanitizeForShell(message);
|
|
83
|
+
spawnDetached('osascript', [
|
|
84
|
+
'-e',
|
|
85
|
+
`display notification "${safe}" with title "${TITLE}"`,
|
|
86
|
+
]);
|
|
87
|
+
}
|
|
88
|
+
function notifyWindows(message) {
|
|
89
|
+
const safe = sanitizeForShell(message);
|
|
90
|
+
// Try BurntToast if available; fall back to a terminal bell on stderr if
|
|
91
|
+
// PowerShell itself cannot be invoked. Both paths are best-effort — we
|
|
92
|
+
// never observe the exit code.
|
|
93
|
+
const psCommand = `if (Get-Module -ListAvailable -Name BurntToast) { ` +
|
|
94
|
+
`Import-Module BurntToast; New-BurntToastNotification -Text "${TITLE}", "${safe}" ` +
|
|
95
|
+
`} else { [console]::Beep(800, 200) }`;
|
|
96
|
+
const child = child_process.spawn('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', psCommand], { detached: true, stdio: 'ignore', windowsHide: true });
|
|
97
|
+
child.on('error', () => {
|
|
98
|
+
try {
|
|
99
|
+
process.stderr.write('\x07');
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
// give up silently
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
child.unref();
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Fire an OS-native notification on `input_requested` events for bootstrap
|
|
109
|
+
* loops. Returns immediately when:
|
|
110
|
+
* - the event is not `input_requested`,
|
|
111
|
+
* - the env-var opt-in is missing,
|
|
112
|
+
* - the loop's protocol preset is not `bootstrap`,
|
|
113
|
+
* - the host platform has no supported notifier.
|
|
114
|
+
*
|
|
115
|
+
* Never throws. The cwd parameter is accepted for parity with other hooks
|
|
116
|
+
* but currently unused — the hook decides everything from the event + loop
|
|
117
|
+
* snapshot the caller already loaded.
|
|
118
|
+
*/
|
|
119
|
+
export function notifyOperatorOnInputRequested(event, loop, cwd) {
|
|
120
|
+
void cwd;
|
|
121
|
+
try {
|
|
122
|
+
if (event.kind !== 'input_requested')
|
|
123
|
+
return;
|
|
124
|
+
if (!isEnabled())
|
|
125
|
+
return;
|
|
126
|
+
if (!isBootstrapLoop(loop))
|
|
127
|
+
return;
|
|
128
|
+
const message = composeMessage(event, loop);
|
|
129
|
+
switch (process.platform) {
|
|
130
|
+
case 'linux':
|
|
131
|
+
notifyLinux(message);
|
|
132
|
+
return;
|
|
133
|
+
case 'darwin':
|
|
134
|
+
notifyMac(message);
|
|
135
|
+
return;
|
|
136
|
+
case 'win32':
|
|
137
|
+
notifyWindows(message);
|
|
138
|
+
return;
|
|
139
|
+
default:
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
// Hook is best-effort — swallow any unexpected error so the journal
|
|
145
|
+
// write that triggered us stays the source of truth.
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
//# sourceMappingURL=notify-operator.js.map
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
const DEFAULT_MAX_BYTES = 50 * 1024;
|
|
4
|
+
const MAX_INDIVIDUAL_FILE_BYTES = 1024 * 1024;
|
|
5
|
+
export function readSurveySources(cwd, opts) {
|
|
6
|
+
const cap = opts?.maxBytes ?? DEFAULT_MAX_BYTES;
|
|
7
|
+
const log = [];
|
|
8
|
+
const result = {
|
|
9
|
+
excerpts: [],
|
|
10
|
+
total_byte_count: 0,
|
|
11
|
+
cap_exceeded: false,
|
|
12
|
+
cap_bytes: cap,
|
|
13
|
+
reasoning_log: log,
|
|
14
|
+
};
|
|
15
|
+
let rootEntries;
|
|
16
|
+
try {
|
|
17
|
+
rootEntries = fs.readdirSync(cwd, { withFileTypes: true });
|
|
18
|
+
}
|
|
19
|
+
catch (err) {
|
|
20
|
+
log.push(`could not read project root: ${err.message}`);
|
|
21
|
+
return result;
|
|
22
|
+
}
|
|
23
|
+
const readmeFiles = [];
|
|
24
|
+
const licenseFiles = [];
|
|
25
|
+
for (const entry of rootEntries) {
|
|
26
|
+
if (!entry.isFile())
|
|
27
|
+
continue;
|
|
28
|
+
const upper = entry.name.toUpperCase();
|
|
29
|
+
if (/^README.*\.MD$/.test(upper)) {
|
|
30
|
+
readmeFiles.push(entry.name);
|
|
31
|
+
}
|
|
32
|
+
else if (/^LICENSE(\.MD|\.TXT)?$/.test(upper)) {
|
|
33
|
+
licenseFiles.push(entry.name);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
readmeFiles.sort();
|
|
37
|
+
licenseFiles.sort();
|
|
38
|
+
const candidates = [...readmeFiles, ...licenseFiles];
|
|
39
|
+
const entryPoint = detectEntryPoint(cwd, rootEntries, log);
|
|
40
|
+
if (entryPoint) {
|
|
41
|
+
if (!candidates.includes(entryPoint))
|
|
42
|
+
candidates.push(entryPoint);
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
log.push('no manifest-referenced entry point found');
|
|
46
|
+
}
|
|
47
|
+
for (const relPath of candidates) {
|
|
48
|
+
const absPath = path.join(cwd, relPath);
|
|
49
|
+
let stat;
|
|
50
|
+
try {
|
|
51
|
+
stat = fs.statSync(absPath);
|
|
52
|
+
}
|
|
53
|
+
catch (err) {
|
|
54
|
+
log.push(`skipped ${relPath}: ${err.message}`);
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (!stat.isFile()) {
|
|
58
|
+
log.push(`skipped ${relPath}: not a regular file`);
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (stat.size > MAX_INDIVIDUAL_FILE_BYTES) {
|
|
62
|
+
log.push(`skipped ${relPath}: size ${stat.size} bytes exceeds 1MB individual safety cap`);
|
|
63
|
+
result.cap_exceeded = true;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
let content;
|
|
67
|
+
try {
|
|
68
|
+
content = fs.readFileSync(absPath, 'utf8');
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
log.push(`skipped ${relPath}: UTF-8 read failed (${err.message})`);
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
const remaining = cap - result.total_byte_count;
|
|
75
|
+
if (remaining <= 0) {
|
|
76
|
+
log.push(`skipped ${relPath}: cap of ${cap} bytes already reached`);
|
|
77
|
+
result.cap_exceeded = true;
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
const normalized = relPath.replace(/\\/g, '/');
|
|
81
|
+
const fileBytes = Buffer.byteLength(content, 'utf8');
|
|
82
|
+
if (fileBytes <= remaining) {
|
|
83
|
+
result.excerpts.push({
|
|
84
|
+
file: normalized,
|
|
85
|
+
byte_count: fileBytes,
|
|
86
|
+
body_truncated: false,
|
|
87
|
+
body: content,
|
|
88
|
+
});
|
|
89
|
+
result.total_byte_count += fileBytes;
|
|
90
|
+
log.push(`included ${normalized} (${fileBytes} bytes)`);
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
const truncated = truncateToBytes(content, remaining);
|
|
94
|
+
const truncatedBytes = Buffer.byteLength(truncated, 'utf8');
|
|
95
|
+
result.excerpts.push({
|
|
96
|
+
file: normalized,
|
|
97
|
+
byte_count: truncatedBytes,
|
|
98
|
+
body_truncated: true,
|
|
99
|
+
body: truncated,
|
|
100
|
+
});
|
|
101
|
+
result.total_byte_count += truncatedBytes;
|
|
102
|
+
result.cap_exceeded = true;
|
|
103
|
+
log.push(`truncated ${normalized} from ${fileBytes} to ${truncatedBytes} bytes to fit cap of ${cap}`);
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return result;
|
|
108
|
+
}
|
|
109
|
+
function truncateToBytes(s, maxBytes) {
|
|
110
|
+
const buf = Buffer.from(s, 'utf8');
|
|
111
|
+
if (buf.length <= maxBytes)
|
|
112
|
+
return s;
|
|
113
|
+
let end = maxBytes;
|
|
114
|
+
while (end > 0 && (buf[end] & 0xc0) === 0x80)
|
|
115
|
+
end--;
|
|
116
|
+
return buf.subarray(0, end).toString('utf8');
|
|
117
|
+
}
|
|
118
|
+
function detectEntryPoint(cwd, rootEntries, log) {
|
|
119
|
+
const fileNames = new Set();
|
|
120
|
+
for (const entry of rootEntries) {
|
|
121
|
+
if (entry.isFile())
|
|
122
|
+
fileNames.add(entry.name);
|
|
123
|
+
}
|
|
124
|
+
for (const name of fileNames) {
|
|
125
|
+
if (name.endsWith('.spec')) {
|
|
126
|
+
const found = parsePyinstallerSpec(path.join(cwd, name), log);
|
|
127
|
+
if (found)
|
|
128
|
+
return found;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (fileNames.has('package.json')) {
|
|
132
|
+
const found = parsePackageJson(path.join(cwd, 'package.json'), log);
|
|
133
|
+
if (found)
|
|
134
|
+
return found;
|
|
135
|
+
}
|
|
136
|
+
if (fileNames.has('pyproject.toml')) {
|
|
137
|
+
const found = parsePyproject(path.join(cwd, 'pyproject.toml'), log);
|
|
138
|
+
if (found)
|
|
139
|
+
return found;
|
|
140
|
+
}
|
|
141
|
+
if (fileNames.has('Cargo.toml')) {
|
|
142
|
+
const found = parseCargoToml(path.join(cwd, 'Cargo.toml'), cwd, log);
|
|
143
|
+
if (found)
|
|
144
|
+
return found;
|
|
145
|
+
}
|
|
146
|
+
if (fileNames.has('go.mod')) {
|
|
147
|
+
if (fileNames.has('main.go'))
|
|
148
|
+
return 'main.go';
|
|
149
|
+
const found = findGoCmdMain(cwd);
|
|
150
|
+
if (found)
|
|
151
|
+
return found;
|
|
152
|
+
}
|
|
153
|
+
return undefined;
|
|
154
|
+
}
|
|
155
|
+
function parsePyinstallerSpec(file, log) {
|
|
156
|
+
let text;
|
|
157
|
+
try {
|
|
158
|
+
text = fs.readFileSync(file, 'utf8');
|
|
159
|
+
}
|
|
160
|
+
catch (err) {
|
|
161
|
+
log.push(`pyinstaller .spec read failed: ${err.message}`);
|
|
162
|
+
return undefined;
|
|
163
|
+
}
|
|
164
|
+
const m = /Analysis\(\s*\[\s*['"]([^'"]+)['"]/.exec(text);
|
|
165
|
+
return m ? m[1] : undefined;
|
|
166
|
+
}
|
|
167
|
+
function parsePackageJson(file, log) {
|
|
168
|
+
let parsed;
|
|
169
|
+
try {
|
|
170
|
+
parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
171
|
+
}
|
|
172
|
+
catch (err) {
|
|
173
|
+
log.push(`package.json parse failed: ${err.message}`);
|
|
174
|
+
return undefined;
|
|
175
|
+
}
|
|
176
|
+
if (!parsed || typeof parsed !== 'object')
|
|
177
|
+
return undefined;
|
|
178
|
+
const obj = parsed;
|
|
179
|
+
if (typeof obj.main === 'string')
|
|
180
|
+
return obj.main;
|
|
181
|
+
if (typeof obj.bin === 'string')
|
|
182
|
+
return obj.bin;
|
|
183
|
+
if (obj.bin && typeof obj.bin === 'object') {
|
|
184
|
+
for (const v of Object.values(obj.bin)) {
|
|
185
|
+
if (typeof v === 'string')
|
|
186
|
+
return v;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return undefined;
|
|
190
|
+
}
|
|
191
|
+
function parsePyproject(file, log) {
|
|
192
|
+
let text;
|
|
193
|
+
try {
|
|
194
|
+
text = fs.readFileSync(file, 'utf8');
|
|
195
|
+
}
|
|
196
|
+
catch (err) {
|
|
197
|
+
log.push(`pyproject.toml read failed: ${err.message}`);
|
|
198
|
+
return undefined;
|
|
199
|
+
}
|
|
200
|
+
for (const header of ['[project.scripts]', '[tool.poetry.scripts]']) {
|
|
201
|
+
const idx = text.indexOf(header);
|
|
202
|
+
if (idx === -1)
|
|
203
|
+
continue;
|
|
204
|
+
const rest = text.slice(idx + header.length);
|
|
205
|
+
const nextSectionRel = rest.search(/\n\s*\[/);
|
|
206
|
+
const section = nextSectionRel >= 0 ? rest.slice(0, nextSectionRel) : rest;
|
|
207
|
+
for (const line of section.split('\n')) {
|
|
208
|
+
const trimmed = line.trim();
|
|
209
|
+
if (!trimmed || trimmed.startsWith('#'))
|
|
210
|
+
continue;
|
|
211
|
+
const m = /^[A-Za-z_][\w-]*\s*=\s*['"]([^'"]+)['"]/.exec(trimmed);
|
|
212
|
+
if (m)
|
|
213
|
+
return moduleSpecToPath(m[1]);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return undefined;
|
|
217
|
+
}
|
|
218
|
+
function moduleSpecToPath(spec) {
|
|
219
|
+
const modulePart = spec.split(':')[0];
|
|
220
|
+
return modulePart.split('.').join('/') + '.py';
|
|
221
|
+
}
|
|
222
|
+
function parseCargoToml(file, cwd, log) {
|
|
223
|
+
let text;
|
|
224
|
+
try {
|
|
225
|
+
text = fs.readFileSync(file, 'utf8');
|
|
226
|
+
}
|
|
227
|
+
catch (err) {
|
|
228
|
+
log.push(`Cargo.toml read failed: ${err.message}`);
|
|
229
|
+
return undefined;
|
|
230
|
+
}
|
|
231
|
+
const binMatch = /\[\[bin\]\][\s\S]*?path\s*=\s*['"]([^'"]+)['"]/.exec(text);
|
|
232
|
+
if (binMatch)
|
|
233
|
+
return binMatch[1];
|
|
234
|
+
if (fs.existsSync(path.join(cwd, 'src', 'main.rs')))
|
|
235
|
+
return 'src/main.rs';
|
|
236
|
+
return undefined;
|
|
237
|
+
}
|
|
238
|
+
function findGoCmdMain(cwd) {
|
|
239
|
+
const cmdDir = path.join(cwd, 'cmd');
|
|
240
|
+
let entries;
|
|
241
|
+
try {
|
|
242
|
+
entries = fs.readdirSync(cmdDir, { withFileTypes: true });
|
|
243
|
+
}
|
|
244
|
+
catch {
|
|
245
|
+
return undefined;
|
|
246
|
+
}
|
|
247
|
+
for (const entry of entries) {
|
|
248
|
+
if (!entry.isDirectory())
|
|
249
|
+
continue;
|
|
250
|
+
const candidate = path.join(cmdDir, entry.name, 'main.go');
|
|
251
|
+
if (fs.existsSync(candidate))
|
|
252
|
+
return path.posix.join('cmd', entry.name, 'main.go');
|
|
253
|
+
}
|
|
254
|
+
return undefined;
|
|
255
|
+
}
|
|
256
|
+
//# sourceMappingURL=survey-source-reader.js.map
|
package/dist/core/loops/index.js
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
export * from './types.js';
|
|
2
|
-
export { closeLoop, ensureLoopsDir, generateLoopId, generateMutationId, generateSlotId, getLoop, listLoopEvents, listLoops, openLoop, } from './store.js';
|
|
3
|
-
export { add_artifact, advance, complete_turn, evaluatePhaseAdvanceGate, evaluateStopCondition, pause, resume, turn, } from './verbs.js';
|
|
2
|
+
export { AwaitingFileApplyApprovalError, closeLoop, ensureLoopsDir, generateLoopId, generateMutationId, generateSlotId, getLoop, listLoopEvents, listLoops, openLoop, writeThreadFile, } from './store.js';
|
|
3
|
+
export { add_artifact, advance, complete_turn, evaluatePhaseAdvanceGate, evaluateStopCondition, pause, provideInput, reconcileOpenQuestions, requestInput, resume, sweepPauseTimeouts, turn, } from './verbs.js';
|
|
4
4
|
export { decideNextPhase, artifactsInIteration, noNewCritiqueInIteration, hasCriticSignalInIteration, } from './iteration-engine.js';
|
|
5
|
+
export { computeNextExpected, } from './next-expected.js';
|
|
5
6
|
export { buildIdeationBrief, } from './brief-assembly.js';
|
|
7
|
+
export { BOOTSTRAP_PRESET } from './presets/bootstrap.js';
|
|
8
|
+
export { writeProjectMdSafe, } from './hooks/bootstrap-write.js';
|
|
9
|
+
export { notifyOperatorOnInputRequested } from './hooks/notify-operator.js';
|
|
10
|
+
export { readSurveySources, } from './hooks/survey-source-reader.js';
|
|
6
11
|
export { acquireLock, hashRequest, recordConflict, withLoopLock, DEFAULT_MAX_MUTATION_DURATION_MS, IDEMPOTENCY_TTL_MS, LEASE_GRACE_MS, LEASE_WINDOW_MS, IdempotencyKeyReusedError, IdempotencyOwnerMismatchError, LockLostError, LockTimeoutError, VersionConflictError, } from './lock.js';
|
|
12
|
+
export { acquireBootstrapLoop, findExistingBootstrapLoop, BootstrapCoordinationInProgressError, } from './bootstrap-acquire.js';
|
|
7
13
|
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
export function computeNextExpected(loop) {
|
|
2
|
+
if (loop.status === 'completed' || loop.status === 'cancelled' || loop.status === 'blocked') {
|
|
3
|
+
return null;
|
|
4
|
+
}
|
|
5
|
+
// pln#508 step 3 fix from codex review (loop-handlers.ts:252): paused
|
|
6
|
+
// loops with open operator_questions should hint provide_input, not
|
|
7
|
+
// advance/close. open_questions check fires regardless of status so it
|
|
8
|
+
// also catches the slot-scope case where loop.status === 'open' but a
|
|
9
|
+
// slot is in waiting_input.
|
|
10
|
+
if (loop.open_questions.length > 0) {
|
|
11
|
+
return {
|
|
12
|
+
action: 'provide_input',
|
|
13
|
+
intent: 'bclaw_loop.provide_input',
|
|
14
|
+
reason: loop.status === 'paused' ? loop.pause_reason : 'awaiting_operator',
|
|
15
|
+
blocking_on: [...loop.open_questions],
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
if (loop.status === 'paused') {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
const currentPhaseSlots = loop.slots.filter((s) => (s.phase ?? loop.current_phase) === loop.current_phase);
|
|
22
|
+
const openSlots = currentPhaseSlots.filter((s) => s.status === 'open');
|
|
23
|
+
if (openSlots.length > 0) {
|
|
24
|
+
const first = openSlots[0];
|
|
25
|
+
return {
|
|
26
|
+
action: 'turn',
|
|
27
|
+
intent: 'bclaw_loop.turn',
|
|
28
|
+
phase: loop.current_phase,
|
|
29
|
+
slot_id: first.slot_id,
|
|
30
|
+
role: first.role,
|
|
31
|
+
blocking_on: openSlots.map((s) => s.slot_id),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
const assignedOrWorking = currentPhaseSlots.filter((s) => s.status === 'assigned' || s.status === 'working');
|
|
35
|
+
if (assignedOrWorking.length > 0) {
|
|
36
|
+
return {
|
|
37
|
+
action: 'complete_turn',
|
|
38
|
+
intent: 'bclaw_loop.complete_turn',
|
|
39
|
+
phase: loop.current_phase,
|
|
40
|
+
slot_id: assignedOrWorking[0].slot_id,
|
|
41
|
+
role: assignedOrWorking[0].role,
|
|
42
|
+
blocking_on: assignedOrWorking.map((s) => s.slot_id),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
const phaseNames = loop.phases.map((p) => p.name);
|
|
46
|
+
const currentIndex = phaseNames.indexOf(loop.current_phase);
|
|
47
|
+
if (currentIndex >= 0 && currentIndex + 1 < phaseNames.length) {
|
|
48
|
+
return {
|
|
49
|
+
action: 'advance',
|
|
50
|
+
intent: 'bclaw_loop.advance',
|
|
51
|
+
from_phase: loop.current_phase,
|
|
52
|
+
to_phase: phaseNames[currentIndex + 1],
|
|
53
|
+
blocking_on: [],
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
action: 'close',
|
|
58
|
+
intent: 'bclaw_loop.close',
|
|
59
|
+
reason: 'terminal_phase_reached',
|
|
60
|
+
blocking_on: [],
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=next-expected.js.map
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pln#511 step 1 — bootstrap preset.
|
|
3
|
+
*
|
|
4
|
+
* Phase chain (Phase 0 spec):
|
|
5
|
+
* survey → produce signals_report from existing project memory.
|
|
6
|
+
* propose → first-draft PROJECT.md from survey + freeform context.
|
|
7
|
+
* clarify → at most one round of operator questions; advance once
|
|
8
|
+
* open_questions drains OR the cap fires.
|
|
9
|
+
* review_draft→ wait for the operator's verdict / answers.
|
|
10
|
+
* converge → emit the final project_md_final and close.
|
|
11
|
+
*
|
|
12
|
+
* `max_operator_questions=3` and `max_pause_duration='P7D'` match the
|
|
13
|
+
* Phase 0 spec defaults (mitigates feedback_agent_autonomy_gap.md — agents
|
|
14
|
+
* must not defer everything to the human).
|
|
15
|
+
*/
|
|
16
|
+
export const BOOTSTRAP_PRESET = {
|
|
17
|
+
phases: [
|
|
18
|
+
// pln#516 step 1 — survey reads source. The bootstrap champion SHOULD
|
|
19
|
+
// call `readSurveySources(cwd)` (exported from ../hooks/survey-source-reader.js)
|
|
20
|
+
// to populate the source_excerpts portion of its signals_report. The engine
|
|
21
|
+
// does NOT invoke this automatically; the champion drives it (RefBasedArtifactBody
|
|
22
|
+
// is unchanged — see types.ts). Empirical motivation: TranslaVox cold-start
|
|
23
|
+
// missed the actual GCP Speech+Translate pipeline because survey scanned only
|
|
24
|
+
// topology + manifests (can_0160d6c4).
|
|
25
|
+
{
|
|
26
|
+
name: 'survey',
|
|
27
|
+
context_filter: ['project_vision', 'decisions', 'plans', 'feedback'],
|
|
28
|
+
advance_gate: { kind: 'artifact_produced', phase: 'survey', type: 'signals_report' },
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
name: 'propose',
|
|
32
|
+
context_filter: ['*'],
|
|
33
|
+
advance_gate: { kind: 'artifact_produced', phase: 'propose', type: 'project_md_draft' },
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
name: 'clarify',
|
|
37
|
+
context_filter: ['critique_history', 'runtime_notes', 'feedback'],
|
|
38
|
+
// pln#516 step 2 — wrap the original `any` exit condition under a
|
|
39
|
+
// `min_iterations >= 1` floor so the gate refuses to auto-traverse at
|
|
40
|
+
// entry. Without the floor `no_open_questions` is trivially true while
|
|
41
|
+
// `open_questions` is still `[]` (it only fills once the champion calls
|
|
42
|
+
// requestInput), and `advance(propose→clarify)` slid straight into
|
|
43
|
+
// review_draft (can_d5a41770, run_4b0500c6).
|
|
44
|
+
advance_gate: {
|
|
45
|
+
kind: 'all',
|
|
46
|
+
conditions: [
|
|
47
|
+
{ kind: 'min_iterations', n: 1 },
|
|
48
|
+
{
|
|
49
|
+
kind: 'any',
|
|
50
|
+
conditions: [
|
|
51
|
+
{ kind: 'no_open_questions' },
|
|
52
|
+
{ kind: 'max_iterations', n: 1 },
|
|
53
|
+
],
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
name: 'review_draft',
|
|
60
|
+
context_filter: ['*'],
|
|
61
|
+
advance_gate: { kind: 'artifact_produced', phase: 'review_draft', type: 'operator_answer' },
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
name: 'converge',
|
|
65
|
+
context_filter: ['*'],
|
|
66
|
+
},
|
|
67
|
+
],
|
|
68
|
+
stop_condition: { kind: 'artifact_produced', phase: 'converge', type: 'project_md_final' },
|
|
69
|
+
protocol: {
|
|
70
|
+
preset: 'bootstrap',
|
|
71
|
+
max_operator_questions: 3,
|
|
72
|
+
max_pause_duration: 'P7D',
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
//# sourceMappingURL=bootstrap.js.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { BOOTSTRAP_PRESET } from './bootstrap.js';
|
|
2
|
+
/**
|
|
3
|
+
* pln#511 step 2 — loop preset registry.
|
|
4
|
+
*
|
|
5
|
+
* The bclaw_coordinate(intent='ideate') handler resolves a caller-supplied
|
|
6
|
+
* `preset` string against this map. Adding a new preset is a one-line
|
|
7
|
+
* change here (plus the preset module itself). The handler intentionally
|
|
8
|
+
* does not import individual presets — only this registry — so unknown
|
|
9
|
+
* names produce a deterministic `unknown_preset` error referencing the
|
|
10
|
+
* keys exported from this file.
|
|
11
|
+
*/
|
|
12
|
+
export const PRESETS = Object.freeze({
|
|
13
|
+
bootstrap: BOOTSTRAP_PRESET,
|
|
14
|
+
});
|
|
15
|
+
export { BOOTSTRAP_PRESET };
|
|
16
|
+
//# sourceMappingURL=index.js.map
|