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.
Files changed (43) hide show
  1. package/dist/brainclaw-vscode.vsix +0 -0
  2. package/dist/cli.js +124 -7
  3. package/dist/commands/bootstrap-loop.js +206 -0
  4. package/dist/commands/loop.js +156 -0
  5. package/dist/commands/loops-handlers.js +110 -55
  6. package/dist/commands/mcp-read-handlers.js +37 -0
  7. package/dist/commands/mcp.js +621 -202
  8. package/dist/commands/questions.js +180 -0
  9. package/dist/commands/reply.js +190 -0
  10. package/dist/commands/session-end.js +105 -3
  11. package/dist/commands/session-start.js +32 -53
  12. package/dist/commands/switch.js +17 -1
  13. package/dist/core/agentrun-reconciler.js +65 -0
  14. package/dist/core/claims.js +29 -0
  15. package/dist/core/dispatch-status.js +219 -0
  16. package/dist/core/entity-operations.js +128 -9
  17. package/dist/core/execution-adapters.js +38 -2
  18. package/dist/core/facade-schema.js +55 -0
  19. package/dist/core/federation-cloud.js +27 -12
  20. package/dist/core/federation-materialize.js +57 -0
  21. package/dist/core/instruction-templates.js +2 -0
  22. package/dist/core/loops/bootstrap-acquire.js +195 -0
  23. package/dist/core/loops/facade-schema.js +68 -1
  24. package/dist/core/loops/hooks/bootstrap-write.js +144 -0
  25. package/dist/core/loops/hooks/notify-operator.js +148 -0
  26. package/dist/core/loops/hooks/survey-source-reader.js +256 -0
  27. package/dist/core/loops/index.js +8 -2
  28. package/dist/core/loops/next-expected.js +63 -0
  29. package/dist/core/loops/presets/bootstrap.js +75 -0
  30. package/dist/core/loops/presets/index.js +16 -0
  31. package/dist/core/loops/store.js +224 -4
  32. package/dist/core/loops/types.js +346 -1
  33. package/dist/core/loops/verbs.js +739 -6
  34. package/dist/core/schema.js +28 -2
  35. package/dist/core/state.js +62 -0
  36. package/dist/facts.js +7 -5
  37. package/dist/facts.json +6 -4
  38. package/docs/concepts/dispatch-lifecycle.md +228 -0
  39. package/docs/concepts/loop-engine.md +55 -0
  40. package/docs/concepts/multi-agent-workflows.md +167 -166
  41. package/docs/concepts/troubleshooting.md +10 -2
  42. package/docs/integrations/overview.md +14 -12
  43. 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
@@ -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