brainclaw 0.28.0 → 1.5.3
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/README.md +193 -170
- package/dist/brainclaw-vscode.vsix +0 -0
- package/dist/cli.js +683 -23
- package/dist/commands/accept.js +3 -0
- package/dist/commands/add-step.js +11 -26
- package/dist/commands/agent-board.js +70 -3
- package/dist/commands/audit.js +19 -0
- package/dist/commands/check-policy.js +54 -0
- package/dist/commands/check-security-mcp.js +145 -0
- package/dist/commands/check-security.js +106 -0
- package/dist/commands/claim-resource.js +1 -0
- package/dist/commands/codev.js +672 -0
- package/dist/commands/compact.js +74 -0
- package/dist/commands/complete-step.js +16 -26
- package/dist/commands/constraint.js +8 -20
- package/dist/commands/decision.js +9 -20
- package/dist/commands/delete-plan.js +10 -12
- package/dist/commands/delete-step.js +16 -0
- package/dist/commands/dispatch.js +163 -0
- package/dist/commands/doctor.js +1122 -49
- package/dist/commands/enable-agent.js +1 -0
- package/dist/commands/export.js +280 -22
- package/dist/commands/handoff.js +33 -0
- package/dist/commands/harvest.js +189 -0
- package/dist/commands/hooks.js +82 -25
- package/dist/commands/inbox.js +169 -0
- package/dist/commands/init.js +38 -31
- package/dist/commands/install-hooks.js +71 -44
- package/dist/commands/link.js +89 -0
- package/dist/commands/list-claims.js +48 -3
- package/dist/commands/list-plans.js +129 -25
- package/dist/commands/loops-handlers.js +409 -0
- package/dist/commands/mcp-read-handlers.js +1628 -0
- package/dist/commands/mcp-schemas.generated.js +74 -0
- package/dist/commands/mcp.js +4244 -1475
- package/dist/commands/plan-resource.js +64 -0
- package/dist/commands/plan.js +12 -26
- package/dist/commands/prune.js +37 -2
- package/dist/commands/reflect.js +20 -7
- package/dist/commands/release-claim.js +11 -6
- package/dist/commands/release-notes.js +170 -0
- package/dist/commands/repair.js +210 -0
- package/dist/commands/run-profile.js +57 -0
- package/dist/commands/sequence.js +113 -0
- package/dist/commands/session-end.js +423 -14
- package/dist/commands/session-start.js +214 -41
- package/dist/commands/setup-security.js +103 -0
- package/dist/commands/setup.js +42 -4
- package/dist/commands/stale.js +109 -0
- package/dist/commands/switch.js +131 -10
- package/dist/commands/trap.js +14 -31
- package/dist/commands/update-handoff.js +63 -4
- package/dist/commands/update-plan.js +21 -28
- package/dist/commands/update-step.js +37 -0
- package/dist/commands/upgrade.js +313 -6
- package/dist/commands/usage.js +102 -0
- package/dist/commands/version.js +20 -0
- package/dist/commands/who.js +124 -0
- package/dist/commands/worktree.js +105 -0
- package/dist/core/actions.js +315 -0
- package/dist/core/agent-capability.js +610 -17
- package/dist/core/agent-context.js +7 -1
- package/dist/core/agent-files.js +1169 -85
- package/dist/core/agent-integrations.js +160 -5
- package/dist/core/agent-inventory.js +2 -0
- package/dist/core/agent-profiles.js +93 -0
- package/dist/core/agent-registry.js +162 -30
- package/dist/core/agentrun-reconciler.js +345 -0
- package/dist/core/agentruns.js +424 -0
- package/dist/core/ai-agent-detection.js +31 -10
- package/dist/core/archival.js +77 -0
- package/dist/core/assignment-sweeper.js +82 -0
- package/dist/core/assignments.js +367 -0
- package/dist/core/audit.js +30 -0
- package/dist/core/bootstrap.js +61 -10
- package/dist/core/brainclaw-version.js +94 -2
- package/dist/core/candidates.js +93 -2
- package/dist/core/claims.js +419 -0
- package/dist/core/codev-metrics.js +77 -0
- package/dist/core/codev-personas.js +31 -0
- package/dist/core/codev-plan-gen.js +35 -0
- package/dist/core/codev-prompts.js +74 -0
- package/dist/core/codev-responses.js +62 -0
- package/dist/core/codev-rounds.js +218 -0
- package/dist/core/config.js +4 -0
- package/dist/core/context.js +454 -34
- package/dist/core/coordination.js +201 -6
- package/dist/core/cross-project.js +230 -16
- package/dist/core/default-profiles/doctor.yaml +11 -0
- package/dist/core/default-profiles/janitor.yaml +11 -0
- package/dist/core/default-profiles/onboarder.yaml +11 -0
- package/dist/core/default-profiles/reviewer.yaml +13 -0
- package/dist/core/dispatcher.js +1189 -0
- package/dist/core/duplicates.js +2 -2
- package/dist/core/entity-operations.js +450 -0
- package/dist/core/entity-registry.js +344 -0
- package/dist/core/event-log.js +1 -0
- package/dist/core/events.js +106 -2
- package/dist/core/execution-adapters.js +154 -0
- package/dist/core/execution-context.js +63 -0
- package/dist/core/execution-profile.js +270 -0
- package/dist/core/execution.js +255 -0
- package/dist/core/facade-schema.js +81 -0
- package/dist/core/federation-cloud.js +99 -0
- package/dist/core/federation-message.js +52 -0
- package/dist/core/federation-transport.js +65 -0
- package/dist/core/gc-semantic.js +482 -0
- package/dist/core/governance.js +247 -0
- package/dist/core/guards.js +19 -0
- package/dist/core/ideation.js +72 -0
- package/dist/core/identity.js +252 -28
- package/dist/core/ids.js +6 -0
- package/dist/core/input-validation.js +2 -2
- package/dist/core/instruction-templates.js +344 -136
- package/dist/core/io.js +90 -11
- package/dist/core/lock.js +6 -2
- package/dist/core/loops/brief-assembly.js +213 -0
- package/dist/core/loops/facade-schema.js +148 -0
- package/dist/core/loops/index.js +7 -0
- package/dist/core/loops/iteration-engine.js +139 -0
- package/dist/core/loops/lock.js +385 -0
- package/dist/core/loops/store.js +201 -0
- package/dist/core/loops/types.js +403 -0
- package/dist/core/loops/verbs.js +534 -0
- package/dist/core/markdown.js +15 -3
- package/dist/core/memory-compactor.js +432 -0
- package/dist/core/memory-git.js +152 -8
- package/dist/core/messaging.js +278 -0
- package/dist/core/migration.js +32 -1
- package/dist/core/mutation-pipeline.js +4 -2
- package/dist/core/operations/memory-mutation.js +129 -0
- package/dist/core/operations/memory-write.js +78 -0
- package/dist/core/operations/plan.js +190 -0
- package/dist/core/policy.js +169 -0
- package/dist/core/repo-analysis.js +67 -0
- package/dist/core/reputation.js +9 -3
- package/dist/core/schema.js +546 -21
- package/dist/core/search.js +21 -2
- package/dist/core/security-cache.js +71 -0
- package/dist/core/security-guard.js +152 -0
- package/dist/core/security-scoring.js +86 -0
- package/dist/core/sequence.js +130 -0
- package/dist/core/socket-client.js +113 -0
- package/dist/core/staleness.js +246 -0
- package/dist/core/state.js +98 -22
- package/dist/core/store-resolution.js +54 -12
- package/dist/core/toml-writer.js +76 -0
- package/dist/core/upgrades/backup.js +232 -0
- package/dist/core/upgrades/health-check.js +169 -0
- package/dist/core/upgrades/patches/candidate-archive.js +145 -0
- package/dist/core/upgrades/patches/handoff-review-strip.js +128 -0
- package/dist/core/upgrades/patches/provenance-rollout.js +136 -0
- package/dist/core/upgrades/schema-version.js +97 -0
- package/dist/core/worktree.js +606 -0
- package/dist/facts.js +114 -0
- package/dist/facts.json +111 -0
- package/docs/architecture/project-refs.md +5 -1
- package/docs/cli.md +690 -43
- package/docs/concepts/ideation-loop.md +317 -0
- package/docs/concepts/loop-engine.md +456 -0
- package/docs/concepts/mcp-governance.md +268 -0
- package/docs/concepts/memory-staleness.md +122 -0
- package/docs/concepts/multi-agent-workflows.md +166 -0
- package/docs/concepts/plans-and-claims.md +31 -6
- package/docs/concepts/project-md-convention.md +35 -0
- package/docs/concepts/troubleshooting.md +220 -0
- package/docs/concepts/upgrade-cli.md +202 -0
- package/docs/concepts/upgrade-dogfood-procedure.md +114 -0
- package/docs/context-format-changelog.md +2 -2
- package/docs/context-format.md +2 -2
- package/docs/index.md +68 -0
- package/docs/integrations/agents.md +15 -16
- package/docs/integrations/cline.md +88 -0
- package/docs/integrations/codex.md +75 -23
- package/docs/integrations/continue.md +60 -0
- package/docs/integrations/copilot.md +67 -9
- package/docs/integrations/kilocode.md +72 -0
- package/docs/integrations/mcp.md +304 -21
- package/docs/integrations/mistral-vibe.md +122 -0
- package/docs/integrations/opencode.md +84 -0
- package/docs/integrations/overview.md +23 -8
- package/docs/integrations/roo.md +74 -0
- package/docs/integrations/windsurf.md +83 -0
- package/docs/mcp-schema-changelog.md +191 -1
- package/docs/playbooks/integration/index.md +121 -0
- package/docs/playbooks/productivity/index.md +102 -0
- package/docs/playbooks/team/index.md +122 -0
- package/docs/product/agent-first-model.md +184 -0
- package/docs/product/entity-model-audit.md +462 -0
- package/docs/quickstart-existing-project.md +135 -0
- package/docs/quickstart.md +124 -37
- package/docs/release-maintenance.md +79 -0
- package/docs/review.md +2 -0
- package/docs/server-operations.md +118 -0
- package/package.json +20 -12
- package/dist/commands/claude-desktop-extension.js +0 -18
- package/dist/commands/diff.js +0 -99
- package/dist/core/claude-desktop-extension.js +0 -224
package/dist/core/io.js
CHANGED
|
@@ -22,9 +22,11 @@ const ENTITY_DIR_MAP = {
|
|
|
22
22
|
'instructions': 'memory/instructions',
|
|
23
23
|
// coordination/ — Agent↔Project: active work state
|
|
24
24
|
'plans': 'coordination/plans',
|
|
25
|
+
'sequences': 'coordination/sequences',
|
|
25
26
|
'claims': 'coordination/claims',
|
|
26
27
|
'handoffs': 'coordination/handoffs',
|
|
27
28
|
'sessions': 'coordination/sessions',
|
|
29
|
+
// Shared root: pending candidate JSONs live at inbox/, agent messages at inbox/{agent}/.
|
|
28
30
|
'inbox': 'coordination/inbox',
|
|
29
31
|
'inbox/accepted': 'coordination/inbox/accepted',
|
|
30
32
|
'inbox/rejected': 'coordination/inbox/rejected',
|
|
@@ -32,6 +34,9 @@ const ENTITY_DIR_MAP = {
|
|
|
32
34
|
'runtime-hosts': 'coordination/runtime-hosts',
|
|
33
35
|
'runtime-private': 'coordination/runtime-private',
|
|
34
36
|
'surface-tasks': 'coordination/surface-tasks',
|
|
37
|
+
'assignments': 'coordination/assignments',
|
|
38
|
+
'runs': 'coordination/runs',
|
|
39
|
+
'actions': 'coordination/actions',
|
|
35
40
|
// discovery/ — Project entity: what's available
|
|
36
41
|
'bootstrap': 'discovery/bootstrap',
|
|
37
42
|
'bootstrap/seeds': 'discovery/bootstrap/seeds',
|
|
@@ -78,25 +83,97 @@ export function memoryPath(filename, cwd, preferredDirName) {
|
|
|
78
83
|
return path.join(memoryDir(cwd, preferredDirName), filename);
|
|
79
84
|
}
|
|
80
85
|
export function storeLockPath(cwd, preferredDirName) {
|
|
81
|
-
|
|
86
|
+
const root = cwd ?? process.cwd();
|
|
87
|
+
const dirName = preferredDirName ?? MEMORY_DIR;
|
|
88
|
+
// Keep the store-wide lock alongside the store root so it survives
|
|
89
|
+
// upgrade park/swap renames. Writers and upgrade/rollback all share
|
|
90
|
+
// this stable target.
|
|
91
|
+
return path.join(root, `${dirName}${STORE_LOCK_BASENAME}`);
|
|
82
92
|
}
|
|
83
93
|
export function memoryExists(cwd, preferredDirName) {
|
|
84
94
|
return fs.existsSync(memoryDir(cwd, preferredDirName));
|
|
85
95
|
}
|
|
96
|
+
/**
|
|
97
|
+
* Read the project vision from the first available source:
|
|
98
|
+
* 1. PROJECT.md at workspace root (human-written, canonical)
|
|
99
|
+
* 2. .brainclaw/project.md first non-header paragraph (legacy project.md export)
|
|
100
|
+
* Returns undefined if no vision is found.
|
|
101
|
+
*/
|
|
102
|
+
export function readProjectVision(cwd = process.cwd(), thresholdLines = 20) {
|
|
103
|
+
// 1. PROJECT.md at workspace root — canonical source
|
|
104
|
+
const projectMdPath = path.join(cwd, 'PROJECT.md');
|
|
105
|
+
if (fs.existsSync(projectMdPath)) {
|
|
106
|
+
try {
|
|
107
|
+
const content = fs.readFileSync(projectMdPath, 'utf-8').trim();
|
|
108
|
+
if (content) {
|
|
109
|
+
const lines = content.split('\n');
|
|
110
|
+
if (lines.length <= thresholdLines) {
|
|
111
|
+
return content;
|
|
112
|
+
}
|
|
113
|
+
return `> **Project Domain Rules**\n> This project maintains detailed domain rules and architecture externally to avoid context bloat.\n> You MUST read \`PROJECT.md\` in the workspace root to understand the project constraints, tech stack, and conventions before coding.`;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
catch { /* fall through */ }
|
|
117
|
+
}
|
|
118
|
+
// 2. .brainclaw/project.md — extract description from first paragraph after title
|
|
119
|
+
const legacyPath = path.join(cwd, MEMORY_DIR, 'project.md');
|
|
120
|
+
if (fs.existsSync(legacyPath)) {
|
|
121
|
+
try {
|
|
122
|
+
const content = fs.readFileSync(legacyPath, 'utf-8');
|
|
123
|
+
const vision = extractVisionFromProjectMd(content);
|
|
124
|
+
if (vision)
|
|
125
|
+
return vision;
|
|
126
|
+
}
|
|
127
|
+
catch { /* fall through */ }
|
|
128
|
+
}
|
|
129
|
+
return undefined;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Extract the vision paragraph from the legacy .brainclaw/project.md format.
|
|
133
|
+
* Looks for a description/vision section or the first non-header, non-list paragraph.
|
|
134
|
+
*/
|
|
135
|
+
function extractVisionFromProjectMd(content) {
|
|
136
|
+
const lines = content.split('\n');
|
|
137
|
+
// Look for a line that starts with a descriptive phrase, skip headers and list items
|
|
138
|
+
const descriptionLines = [];
|
|
139
|
+
let inSection = false;
|
|
140
|
+
for (const line of lines) {
|
|
141
|
+
const trimmed = line.trim();
|
|
142
|
+
// Skip empty lines, markdown headers, sentinel lines, and list items at start
|
|
143
|
+
if (!trimmed) {
|
|
144
|
+
if (inSection && descriptionLines.length > 0)
|
|
145
|
+
break; // end of paragraph
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
if (trimmed.startsWith('#') || trimmed.startsWith('>') || trimmed.startsWith('- **['))
|
|
149
|
+
continue;
|
|
150
|
+
if (trimmed.startsWith('- (none)'))
|
|
151
|
+
continue;
|
|
152
|
+
// Found a content line
|
|
153
|
+
inSection = true;
|
|
154
|
+
descriptionLines.push(trimmed);
|
|
155
|
+
}
|
|
156
|
+
return descriptionLines.length > 0 ? descriptionLines.join('\n') : undefined;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Canonical list of entity-aligned subdirectories expected under `.brainclaw/`.
|
|
160
|
+
* Exposed so doctor + repair flows can audit presence without duplicating the
|
|
161
|
+
* list (pln#397 stp_b5337e30).
|
|
162
|
+
*/
|
|
163
|
+
export const REQUIRED_ENTITY_SUBDIRS = [
|
|
164
|
+
'memory/constraints', 'memory/decisions', 'memory/traps', 'memory/instructions',
|
|
165
|
+
'coordination/plans', 'coordination/sequences', 'coordination/claims', 'coordination/handoffs', 'coordination/sessions',
|
|
166
|
+
'coordination/inbox',
|
|
167
|
+
'discovery',
|
|
168
|
+
'agents',
|
|
169
|
+
];
|
|
86
170
|
export function ensureMemoryDir(cwd, preferredDirName) {
|
|
87
171
|
const dir = memoryDir(cwd, preferredDirName);
|
|
88
172
|
if (!fs.existsSync(dir)) {
|
|
89
173
|
fs.mkdirSync(dir, { recursive: true });
|
|
90
174
|
}
|
|
91
175
|
// Ensure entity-aligned subdirectories exist
|
|
92
|
-
const
|
|
93
|
-
'memory/constraints', 'memory/decisions', 'memory/traps', 'memory/instructions',
|
|
94
|
-
'coordination/plans', 'coordination/claims', 'coordination/handoffs', 'coordination/sessions',
|
|
95
|
-
'coordination/inbox',
|
|
96
|
-
'discovery',
|
|
97
|
-
'agents',
|
|
98
|
-
];
|
|
99
|
-
for (const subdir of entityDirs) {
|
|
176
|
+
for (const subdir of REQUIRED_ENTITY_SUBDIRS) {
|
|
100
177
|
const p = path.join(dir, subdir);
|
|
101
178
|
if (!fs.existsSync(p)) {
|
|
102
179
|
fs.mkdirSync(p, { recursive: true });
|
|
@@ -104,8 +181,10 @@ export function ensureMemoryDir(cwd, preferredDirName) {
|
|
|
104
181
|
}
|
|
105
182
|
}
|
|
106
183
|
export function withStoreLock(cwd = process.cwd(), fn, preferredDirName) {
|
|
107
|
-
|
|
108
|
-
|
|
184
|
+
return withLock(storeLockPath(cwd, preferredDirName), () => {
|
|
185
|
+
ensureMemoryDir(cwd, preferredDirName);
|
|
186
|
+
return fn();
|
|
187
|
+
});
|
|
109
188
|
}
|
|
110
189
|
/** Check if a path is a file, or a directory with at least one entry. */
|
|
111
190
|
function hasContent(p) {
|
package/dist/core/lock.js
CHANGED
|
@@ -37,8 +37,12 @@ function tryCreateLock(lockPath) {
|
|
|
37
37
|
return true;
|
|
38
38
|
}
|
|
39
39
|
catch (err) {
|
|
40
|
-
if (err instanceof Error && 'code' in err
|
|
41
|
-
|
|
40
|
+
if (err instanceof Error && 'code' in err) {
|
|
41
|
+
const code = err.code;
|
|
42
|
+
if (code === 'EEXIST' || code === 'EPERM' || code === 'EACCES') {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
42
46
|
throw err;
|
|
43
47
|
}
|
|
44
48
|
}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pln#492 phase 2.d.1 — Critic brief assembly (pure function).
|
|
3
|
+
*
|
|
4
|
+
* Builds the message a slot receives when its phase fires. Honours the
|
|
5
|
+
* phase's `context_filter` so the critic sees adversarial memory only
|
|
6
|
+
* (traps + feedback + runtime_notes + critique_history), the proposal
|
|
7
|
+
* sees positive context (decisions + constraints + plans + project_vision),
|
|
8
|
+
* and revision/synthesis see the full bundle via the '*' wildcard.
|
|
9
|
+
*
|
|
10
|
+
* BM25 ranking, top-K, and the underlying memory store are injected via
|
|
11
|
+
* `BriefMemoryProvider` so this module stays testable in isolation. The
|
|
12
|
+
* dispatch wire-up (phase 2.d.2) plugs in a real provider backed by
|
|
13
|
+
* bclaw_search.
|
|
14
|
+
*
|
|
15
|
+
* The bundle is capped at `maxChars` (default 48 000 ≈ 12 000 tokens at
|
|
16
|
+
* ~4 chars/token English) to mitigate trp#179 (oversized payloads on
|
|
17
|
+
* multi-trap projects forcing agents back to CLI fallbacks). Truncation
|
|
18
|
+
* is greedy by category-then-item order, with a "(memory bundle truncated
|
|
19
|
+
* — N items dropped)" tail so the operator can see when content was
|
|
20
|
+
* dropped.
|
|
21
|
+
*/
|
|
22
|
+
const DEFAULT_MAX_CHARS = 48_000;
|
|
23
|
+
const DEFAULT_TOP_K_PER_CATEGORY = 8;
|
|
24
|
+
/**
|
|
25
|
+
* Concrete categories the wildcard '*' resolves to. Excludes loop-internal
|
|
26
|
+
* categories (critique_history / revision_history / synthesis_artifact)
|
|
27
|
+
* because those are sourced from the thread itself, not from the memory
|
|
28
|
+
* provider.
|
|
29
|
+
*/
|
|
30
|
+
const WILDCARD_USER_FACING_CATEGORIES = [
|
|
31
|
+
'traps',
|
|
32
|
+
'feedback',
|
|
33
|
+
'runtime_notes',
|
|
34
|
+
'decisions',
|
|
35
|
+
'constraints',
|
|
36
|
+
'handoffs',
|
|
37
|
+
'plans',
|
|
38
|
+
'candidates',
|
|
39
|
+
'project_vision',
|
|
40
|
+
];
|
|
41
|
+
const LOOP_INTERNAL_CATEGORIES = new Set([
|
|
42
|
+
'critique_history',
|
|
43
|
+
'revision_history',
|
|
44
|
+
'synthesis_artifact',
|
|
45
|
+
]);
|
|
46
|
+
export function buildIdeationBrief(input) {
|
|
47
|
+
const { thread, slotRole, memoryProvider, maxChars = DEFAULT_MAX_CHARS, topKPerCategory = DEFAULT_TOP_K_PER_CATEGORY, } = input;
|
|
48
|
+
const proposal = findProposalArtifact(thread);
|
|
49
|
+
const proposalText = proposal?.body?.trim() ?? '(no proposal seed found)';
|
|
50
|
+
// Resolve which memory categories the current phase wants. If the
|
|
51
|
+
// current phase has no context_filter, fall back to '*' (full bundle).
|
|
52
|
+
const currentPhaseDef = thread.phases.find((p) => p.name === thread.current_phase);
|
|
53
|
+
const requestedCategories = currentPhaseDef?.context_filter ?? ['*'];
|
|
54
|
+
const userFacingCategories = expandUserFacingCategories(requestedCategories);
|
|
55
|
+
const includesLoopInternal = requestedCategories.some((c) => c === '*' || LOOP_INTERNAL_CATEGORIES.has(c));
|
|
56
|
+
const fetchedItemsByCategory = new Map();
|
|
57
|
+
const categoriesUsed = [];
|
|
58
|
+
for (const category of userFacingCategories) {
|
|
59
|
+
const items = memoryProvider.fetch(category, proposalText, topKPerCategory);
|
|
60
|
+
if (items.length > 0) {
|
|
61
|
+
fetchedItemsByCategory.set(category, items);
|
|
62
|
+
categoriesUsed.push(category);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// Loop-internal categories: pulled from thread.artifacts directly.
|
|
66
|
+
// critique_history → all critique artifacts in iterations < current.
|
|
67
|
+
// revision_history → all revision artifacts in iterations < current.
|
|
68
|
+
// synthesis_artifact → the most recent synthesis output (if any).
|
|
69
|
+
const priorArtifactsBlock = includesLoopInternal
|
|
70
|
+
? renderPriorArtifactsBlock(thread, requestedCategories)
|
|
71
|
+
: '';
|
|
72
|
+
const header = renderHeader(thread, slotRole, currentPhaseDef?.name ?? thread.current_phase);
|
|
73
|
+
const proposalBlock = renderProposalBlock(proposalText);
|
|
74
|
+
const memoryBlock = renderMemoryBlock(fetchedItemsByCategory);
|
|
75
|
+
const closing = renderClosingInstructions(slotRole, thread.current_phase);
|
|
76
|
+
// Compose with truncation. The proposal seed and header are fixed;
|
|
77
|
+
// memory + prior artifacts share the remaining budget. Memory before
|
|
78
|
+
// prior-artifacts so the critic always sees fresh adversarial pressure.
|
|
79
|
+
const fixedParts = [header, proposalBlock, closing];
|
|
80
|
+
const fixedSize = fixedParts.reduce((n, s) => n + s.length, 0);
|
|
81
|
+
const remainingBudget = Math.max(0, maxChars - fixedSize);
|
|
82
|
+
const { text: truncatedMemory, truncated, droppedItems, includedItems } = truncateToBudget([memoryBlock, priorArtifactsBlock].filter((s) => s.length > 0), fetchedItemsByCategory, remainingBudget);
|
|
83
|
+
const text = [header, proposalBlock, truncatedMemory, closing]
|
|
84
|
+
.filter((s) => s.length > 0)
|
|
85
|
+
.join('\n\n');
|
|
86
|
+
return { text, truncated, includedItems, droppedItems, categoriesUsed };
|
|
87
|
+
}
|
|
88
|
+
/* ─────────────────────── helpers ─────────────────────── */
|
|
89
|
+
function findProposalArtifact(thread) {
|
|
90
|
+
// Prefer the most recent proposal artifact (in case revisions added
|
|
91
|
+
// updated proposal artifacts later).
|
|
92
|
+
for (let i = thread.artifacts.length - 1; i >= 0; i--) {
|
|
93
|
+
const a = thread.artifacts[i];
|
|
94
|
+
if (a.type === 'proposal')
|
|
95
|
+
return a;
|
|
96
|
+
}
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
function expandUserFacingCategories(requested) {
|
|
100
|
+
if (requested.includes('*')) {
|
|
101
|
+
return [...WILDCARD_USER_FACING_CATEGORIES];
|
|
102
|
+
}
|
|
103
|
+
// Drop loop-internal categories — they're handled separately.
|
|
104
|
+
return requested.filter((c) => !LOOP_INTERNAL_CATEGORIES.has(c) && c !== '*');
|
|
105
|
+
}
|
|
106
|
+
function renderHeader(thread, slotRole, phase) {
|
|
107
|
+
const lines = [
|
|
108
|
+
`# ideation_loop brief`,
|
|
109
|
+
`loop: ${thread.id}`,
|
|
110
|
+
`phase: ${phase}`,
|
|
111
|
+
`iteration: ${thread.iteration_count}`,
|
|
112
|
+
`slot: ${slotRole}`,
|
|
113
|
+
`title: ${thread.title}`,
|
|
114
|
+
];
|
|
115
|
+
if (thread.goal)
|
|
116
|
+
lines.push(`goal: ${thread.goal}`);
|
|
117
|
+
return lines.join('\n');
|
|
118
|
+
}
|
|
119
|
+
function renderProposalBlock(proposalText) {
|
|
120
|
+
return `## proposal\n\n${proposalText}`;
|
|
121
|
+
}
|
|
122
|
+
function renderMemoryBlock(byCategory) {
|
|
123
|
+
if (byCategory.size === 0)
|
|
124
|
+
return '';
|
|
125
|
+
const sections = ['## memory bundle (BM25-ranked, filtered by phase context)'];
|
|
126
|
+
for (const [category, items] of byCategory) {
|
|
127
|
+
sections.push(`### ${category}`);
|
|
128
|
+
for (const item of items) {
|
|
129
|
+
sections.push(`- [${item.id}] ${item.text}`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return sections.join('\n');
|
|
133
|
+
}
|
|
134
|
+
function renderPriorArtifactsBlock(thread, requested) {
|
|
135
|
+
// Critique history is the most useful loop-internal feed for the
|
|
136
|
+
// critique phase: round 2+ critics see what was already raised so they
|
|
137
|
+
// do not duplicate. revision_history gives them the proposer's response.
|
|
138
|
+
const wantsCritique = requested.includes('*') || requested.includes('critique_history');
|
|
139
|
+
const wantsRevision = requested.includes('*') || requested.includes('revision_history');
|
|
140
|
+
const wantsSynthesis = requested.includes('*') || requested.includes('synthesis_artifact');
|
|
141
|
+
const sections = [];
|
|
142
|
+
if (wantsCritique) {
|
|
143
|
+
const priorCritique = thread.artifacts.filter((a) => a.type === 'critique' && (a.iteration ?? 0) < thread.iteration_count);
|
|
144
|
+
if (priorCritique.length > 0) {
|
|
145
|
+
const lines = ['### critique_history (prior iterations)'];
|
|
146
|
+
for (const a of priorCritique) {
|
|
147
|
+
lines.push(`- [${a.artifact_id}] (iter ${a.iteration ?? 0}) ${truncateLine(a.body)}`);
|
|
148
|
+
}
|
|
149
|
+
sections.push(lines.join('\n'));
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
if (wantsRevision) {
|
|
153
|
+
const priorRevision = thread.artifacts.filter((a) => a.phase === 'revision' && (a.iteration ?? 0) < thread.iteration_count);
|
|
154
|
+
if (priorRevision.length > 0) {
|
|
155
|
+
const lines = ['### revision_history (prior iterations)'];
|
|
156
|
+
for (const a of priorRevision) {
|
|
157
|
+
lines.push(`- [${a.artifact_id}] (iter ${a.iteration ?? 0}) ${truncateLine(a.body)}`);
|
|
158
|
+
}
|
|
159
|
+
sections.push(lines.join('\n'));
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (wantsSynthesis) {
|
|
163
|
+
const synthesis = [...thread.artifacts]
|
|
164
|
+
.reverse()
|
|
165
|
+
.find((a) => a.phase === 'synthesis' && a.type === 'plan_draft');
|
|
166
|
+
if (synthesis) {
|
|
167
|
+
sections.push(`### synthesis_artifact\n- [${synthesis.artifact_id}] ${truncateLine(synthesis.body)}`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if (sections.length === 0)
|
|
171
|
+
return '';
|
|
172
|
+
return ['## prior loop artifacts', ...sections].join('\n\n');
|
|
173
|
+
}
|
|
174
|
+
function renderClosingInstructions(slotRole, phase) {
|
|
175
|
+
return [
|
|
176
|
+
`## what to produce`,
|
|
177
|
+
`- Phase "${phase}" expects you to act in role "${slotRole}".`,
|
|
178
|
+
`- Emit findings as LoopArtifacts via bclaw_loop intent='complete_turn' or 'add_artifact'.`,
|
|
179
|
+
`- Cite the memory ids you relied on so the synthesis can audit coverage.`,
|
|
180
|
+
].join('\n');
|
|
181
|
+
}
|
|
182
|
+
function truncateLine(s, maxLen = 200) {
|
|
183
|
+
if (!s)
|
|
184
|
+
return '(no body)';
|
|
185
|
+
return s.length > maxLen ? `${s.slice(0, maxLen)}…` : s;
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Greedy truncation of the variable parts (memory bundle + prior artifacts)
|
|
189
|
+
* to fit the remaining budget. We count items so the caller can warn about
|
|
190
|
+
* dropped content.
|
|
191
|
+
*/
|
|
192
|
+
function truncateToBudget(blocks, byCategory, budget) {
|
|
193
|
+
const totalItems = [...byCategory.values()].reduce((n, arr) => n + arr.length, 0);
|
|
194
|
+
if (blocks.length === 0) {
|
|
195
|
+
return { text: '', truncated: false, includedItems: 0, droppedItems: 0 };
|
|
196
|
+
}
|
|
197
|
+
const joined = blocks.join('\n\n');
|
|
198
|
+
if (joined.length <= budget) {
|
|
199
|
+
return { text: joined, truncated: false, includedItems: totalItems, droppedItems: 0 };
|
|
200
|
+
}
|
|
201
|
+
// Truncate hard at the budget boundary, append a tail noting the cut.
|
|
202
|
+
const tail = '\n\n_(memory bundle truncated to fit the brief size cap; some items were dropped — re-run with a smaller proposal or a per-category top-K to surface the missing ones)_';
|
|
203
|
+
const headRoom = Math.max(0, budget - tail.length);
|
|
204
|
+
const truncatedHead = joined.slice(0, headRoom);
|
|
205
|
+
const text = truncatedHead + tail;
|
|
206
|
+
// Approximate dropped item count: count "- [id]" markers in the
|
|
207
|
+
// dropped tail vs total.
|
|
208
|
+
const droppedTail = joined.slice(headRoom);
|
|
209
|
+
const droppedItems = (droppedTail.match(/^- \[/gm) ?? []).length;
|
|
210
|
+
const includedItems = Math.max(0, totalItems - droppedItems);
|
|
211
|
+
return { text, truncated: true, includedItems, droppedItems };
|
|
212
|
+
}
|
|
213
|
+
//# sourceMappingURL=brief-assembly.js.map
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { LOOP_KINDS, LOOP_STATUSES, LoopLinksSchema, LoopPhaseSchema, LoopRefSchema, LoopSlotSchema, REVIEW_MODES, StopConditionSchema, } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* `bclaw_loop(intent)` request schemas — one per intent, unioned into a
|
|
5
|
+
* discriminated schema. Mirrors the BclawLoopInput type from the v8 RFC.
|
|
6
|
+
*/
|
|
7
|
+
const CallerEnvelopeFields = {
|
|
8
|
+
agent: z.string().optional(),
|
|
9
|
+
agentId: z.string().optional(),
|
|
10
|
+
client_request_id: z.string().min(1).optional(),
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Slot input shape for `bclaw_loop(intent='open')`. Loosens the persisted
|
|
14
|
+
* `LoopSlotSchema` (which requires server-assigned fields like `slot_id`
|
|
15
|
+
* and `status`) so callers only need to supply `role` plus any optional
|
|
16
|
+
* hints. Exported so it can be consumed both by `BclawLoopOpenSchema`
|
|
17
|
+
* below AND by the build-time MCP schema generator (pln#494 phase 2).
|
|
18
|
+
*/
|
|
19
|
+
export const LoopSlotInputSchema = LoopSlotSchema.partial().extend({
|
|
20
|
+
role: z.string().min(1),
|
|
21
|
+
});
|
|
22
|
+
export const BclawLoopOpenSchema = z.object({
|
|
23
|
+
intent: z.literal('open'),
|
|
24
|
+
kind: z.enum(LOOP_KINDS),
|
|
25
|
+
title: z.string().min(1),
|
|
26
|
+
goal: z.string().optional(),
|
|
27
|
+
phases: z.array(LoopPhaseSchema).optional(),
|
|
28
|
+
slots: z.array(LoopSlotInputSchema).optional(),
|
|
29
|
+
linked: LoopLinksSchema.optional(),
|
|
30
|
+
stop_condition: StopConditionSchema.optional(),
|
|
31
|
+
mode: z.enum(REVIEW_MODES).optional(),
|
|
32
|
+
// Opt-in acknowledgement that the caller will drive dispatch manually.
|
|
33
|
+
// Absent (or false) → handler rejects with a pointer to bclaw_coordinate,
|
|
34
|
+
// because a loop opened without a follow-up turn/claim/inbox never runs.
|
|
35
|
+
// See pln#461.
|
|
36
|
+
allow_orphan: z.boolean().optional(),
|
|
37
|
+
...CallerEnvelopeFields,
|
|
38
|
+
});
|
|
39
|
+
export const BclawLoopGetSchema = z.object({
|
|
40
|
+
intent: z.literal('get'),
|
|
41
|
+
loop_id: z.string().regex(/^lop_[0-9a-z]+$/),
|
|
42
|
+
include_events: z.boolean().optional(),
|
|
43
|
+
...CallerEnvelopeFields,
|
|
44
|
+
});
|
|
45
|
+
export const BclawLoopListSchema = z.object({
|
|
46
|
+
intent: z.literal('list'),
|
|
47
|
+
kind: z.enum(LOOP_KINDS).optional(),
|
|
48
|
+
status: z.enum(LOOP_STATUSES).optional(),
|
|
49
|
+
limit: z.number().int().positive().optional(),
|
|
50
|
+
offset: z.number().int().nonnegative().optional(),
|
|
51
|
+
...CallerEnvelopeFields,
|
|
52
|
+
});
|
|
53
|
+
export const BclawLoopTurnSchema = z.object({
|
|
54
|
+
intent: z.literal('turn'),
|
|
55
|
+
loop_id: z.string().regex(/^lop_[0-9a-z]+$/),
|
|
56
|
+
slot_id: z.string().optional(),
|
|
57
|
+
role: z.string().optional(),
|
|
58
|
+
input: z.string().optional(),
|
|
59
|
+
assignment_id: z.string().optional(),
|
|
60
|
+
dispatch: z.boolean().optional(),
|
|
61
|
+
expected_version: z.number().int().nonnegative().optional(),
|
|
62
|
+
...CallerEnvelopeFields,
|
|
63
|
+
});
|
|
64
|
+
export const BclawLoopCompleteTurnSchema = z.object({
|
|
65
|
+
intent: z.literal('complete_turn'),
|
|
66
|
+
loop_id: z.string().regex(/^lop_[0-9a-z]+$/),
|
|
67
|
+
slot_id: z.string().min(1),
|
|
68
|
+
outcome: z.enum(['done', 'failed', 'cancelled']).optional(),
|
|
69
|
+
failure_reason: z.string().optional(),
|
|
70
|
+
artifact: z
|
|
71
|
+
.object({
|
|
72
|
+
phase: z.string().min(1),
|
|
73
|
+
type: z.string().min(1),
|
|
74
|
+
body: z.string().optional(),
|
|
75
|
+
ref: LoopRefSchema.optional(),
|
|
76
|
+
})
|
|
77
|
+
.optional(),
|
|
78
|
+
expected_version: z.number().int().nonnegative().optional(),
|
|
79
|
+
...CallerEnvelopeFields,
|
|
80
|
+
});
|
|
81
|
+
export const BclawLoopAdvanceSchema = z.object({
|
|
82
|
+
intent: z.literal('advance'),
|
|
83
|
+
loop_id: z.string().regex(/^lop_[0-9a-z]+$/),
|
|
84
|
+
to_phase: z.string().optional(),
|
|
85
|
+
reason: z.string().optional(),
|
|
86
|
+
force: z.boolean().optional(),
|
|
87
|
+
expected_version: z.number().int().nonnegative().optional(),
|
|
88
|
+
...CallerEnvelopeFields,
|
|
89
|
+
});
|
|
90
|
+
export const BclawLoopAddArtifactSchema = z.object({
|
|
91
|
+
intent: z.literal('add_artifact'),
|
|
92
|
+
loop_id: z.string().regex(/^lop_[0-9a-z]+$/),
|
|
93
|
+
artifact: z.object({
|
|
94
|
+
phase: z.string().min(1),
|
|
95
|
+
type: z.string().min(1),
|
|
96
|
+
body: z.string().optional(),
|
|
97
|
+
produced_by: z.string().optional(),
|
|
98
|
+
ref: LoopRefSchema.optional(),
|
|
99
|
+
}),
|
|
100
|
+
expected_version: z.number().int().nonnegative().optional(),
|
|
101
|
+
...CallerEnvelopeFields,
|
|
102
|
+
});
|
|
103
|
+
export const BclawLoopPauseSchema = z.object({
|
|
104
|
+
intent: z.literal('pause'),
|
|
105
|
+
loop_id: z.string().regex(/^lop_[0-9a-z]+$/),
|
|
106
|
+
reason: z.string().optional(),
|
|
107
|
+
expected_version: z.number().int().nonnegative().optional(),
|
|
108
|
+
...CallerEnvelopeFields,
|
|
109
|
+
});
|
|
110
|
+
export const BclawLoopResumeSchema = z.object({
|
|
111
|
+
intent: z.literal('resume'),
|
|
112
|
+
loop_id: z.string().regex(/^lop_[0-9a-z]+$/),
|
|
113
|
+
expected_version: z.number().int().nonnegative().optional(),
|
|
114
|
+
...CallerEnvelopeFields,
|
|
115
|
+
});
|
|
116
|
+
export const BclawLoopCloseSchema = z.object({
|
|
117
|
+
intent: z.literal('close'),
|
|
118
|
+
loop_id: z.string().regex(/^lop_[0-9a-z]+$/),
|
|
119
|
+
status: z.enum(['completed', 'cancelled', 'blocked']),
|
|
120
|
+
reason: z.string().optional(),
|
|
121
|
+
expected_version: z.number().int().nonnegative().optional(),
|
|
122
|
+
...CallerEnvelopeFields,
|
|
123
|
+
});
|
|
124
|
+
export const BclawLoopRequestSchema = z.discriminatedUnion('intent', [
|
|
125
|
+
BclawLoopOpenSchema,
|
|
126
|
+
BclawLoopGetSchema,
|
|
127
|
+
BclawLoopListSchema,
|
|
128
|
+
BclawLoopTurnSchema,
|
|
129
|
+
BclawLoopCompleteTurnSchema,
|
|
130
|
+
BclawLoopAdvanceSchema,
|
|
131
|
+
BclawLoopAddArtifactSchema,
|
|
132
|
+
BclawLoopPauseSchema,
|
|
133
|
+
BclawLoopResumeSchema,
|
|
134
|
+
BclawLoopCloseSchema,
|
|
135
|
+
]);
|
|
136
|
+
export const BCLAW_LOOP_INTENTS = [
|
|
137
|
+
'open',
|
|
138
|
+
'get',
|
|
139
|
+
'list',
|
|
140
|
+
'turn',
|
|
141
|
+
'complete_turn',
|
|
142
|
+
'advance',
|
|
143
|
+
'add_artifact',
|
|
144
|
+
'pause',
|
|
145
|
+
'resume',
|
|
146
|
+
'close',
|
|
147
|
+
];
|
|
148
|
+
//# sourceMappingURL=facade-schema.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
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';
|
|
4
|
+
export { decideNextPhase, artifactsInIteration, noNewCritiqueInIteration, hasCriticSignalInIteration, } from './iteration-engine.js';
|
|
5
|
+
export { buildIdeationBrief, } from './brief-assembly.js';
|
|
6
|
+
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';
|
|
7
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pln#492 phase 2.b — Finite state machine for ideation_loop iteration.
|
|
3
|
+
*
|
|
4
|
+
* The driver consults this module before mutating phase state on a loop
|
|
5
|
+
* with an `iteration` block. The engine is pure: it inspects a thread +
|
|
6
|
+
* its protocol shape and returns a structured decision. Applying the
|
|
7
|
+
* decision (mutating thread, appending events, persisting) is the
|
|
8
|
+
* advance() verb's job.
|
|
9
|
+
*
|
|
10
|
+
* Naming the engine an FSM in the file structure is a deliberate
|
|
11
|
+
* implementation discipline (stp_af207293 in pln#492): states = phases,
|
|
12
|
+
* transitions = exit_when conditions + cycle membership, guards = phase
|
|
13
|
+
* advance_gate, actions = system event emissions.
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* Decide the next phase given the current thread state and the protocol.
|
|
17
|
+
*
|
|
18
|
+
* Throws if `current_phase` is not in `protocol.phases`. Throws if there
|
|
19
|
+
* is no successor (last phase + no iteration block). Callers that want
|
|
20
|
+
* to handle "already at end" should check beforehand.
|
|
21
|
+
*/
|
|
22
|
+
export function decideNextPhase(thread, protocol) {
|
|
23
|
+
const phaseNames = protocol.phases.map((p) => p.name);
|
|
24
|
+
const currentIndex = phaseNames.indexOf(thread.current_phase);
|
|
25
|
+
if (currentIndex < 0) {
|
|
26
|
+
throw new Error(`decideNextPhase: current_phase "${thread.current_phase}" not in protocol.phases`);
|
|
27
|
+
}
|
|
28
|
+
const cycle = protocol.iteration?.cycle ?? [];
|
|
29
|
+
const inCycle = cycle.includes(thread.current_phase);
|
|
30
|
+
// Path 1 — current phase is not in the iteration cycle. Linear advance.
|
|
31
|
+
if (!inCycle) {
|
|
32
|
+
if (currentIndex + 1 >= phaseNames.length) {
|
|
33
|
+
throw new Error(`advance: already at last phase "${thread.current_phase}"`);
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
kind: 'advance_to',
|
|
37
|
+
target: phaseNames[currentIndex + 1],
|
|
38
|
+
iteration: thread.iteration_count,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
// Path 2 — current phase is inside the cycle. Two sub-cases:
|
|
42
|
+
// 2a. Not at the end of the cycle yet → step within cycle.
|
|
43
|
+
// 2b. At the end of the cycle → either iterate, exit by exit_when, or
|
|
44
|
+
// exit by max_iterations.
|
|
45
|
+
const cycleIndex = cycle.indexOf(thread.current_phase);
|
|
46
|
+
const atCycleEnd = cycleIndex === cycle.length - 1;
|
|
47
|
+
if (!atCycleEnd) {
|
|
48
|
+
return {
|
|
49
|
+
kind: 'advance_to',
|
|
50
|
+
target: cycle[cycleIndex + 1],
|
|
51
|
+
iteration: thread.iteration_count,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
// 2b — end of cycle. Compute the post-cycle target (phase after the
|
|
55
|
+
// last cycle phase in the protocol order). We use the protocol's
|
|
56
|
+
// declared phase order, not the cycle, because the cycle may be a
|
|
57
|
+
// sub-sequence of phases (e.g. [critique, revision] within
|
|
58
|
+
// [proposal, critique, revision, synthesis]).
|
|
59
|
+
const lastCyclePhaseIndex = phaseNames.indexOf(cycle[cycle.length - 1]);
|
|
60
|
+
if (lastCyclePhaseIndex < 0 || lastCyclePhaseIndex + 1 >= phaseNames.length) {
|
|
61
|
+
throw new Error(`decideNextPhase: cycle's last phase "${cycle[cycle.length - 1]}" has no post-cycle successor`);
|
|
62
|
+
}
|
|
63
|
+
const postCycleTarget = phaseNames[lastCyclePhaseIndex + 1];
|
|
64
|
+
const iterationBlock = protocol.iteration;
|
|
65
|
+
if (!iterationBlock) {
|
|
66
|
+
// Defensive: `inCycle` was true so iteration should be set. If we
|
|
67
|
+
// got here despite that, fall through to a linear advance.
|
|
68
|
+
return {
|
|
69
|
+
kind: 'advance_to',
|
|
70
|
+
target: postCycleTarget,
|
|
71
|
+
iteration: thread.iteration_count,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
// Evaluate exit_when on the just-finished iteration. The current
|
|
75
|
+
// iteration_count is the one that just completed (the engine has not
|
|
76
|
+
// yet incremented).
|
|
77
|
+
if (iterationBlock.exit_when === 'critic_signal' &&
|
|
78
|
+
hasCriticSignalInIteration(thread, thread.iteration_count)) {
|
|
79
|
+
return {
|
|
80
|
+
kind: 'exit_cycle',
|
|
81
|
+
target: postCycleTarget,
|
|
82
|
+
iteration: thread.iteration_count,
|
|
83
|
+
reason: 'critic_signal',
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
if (iterationBlock.exit_when === 'no_new_critique_artifacts' &&
|
|
87
|
+
noNewCritiqueInIteration(thread, thread.iteration_count)) {
|
|
88
|
+
return {
|
|
89
|
+
kind: 'exit_cycle',
|
|
90
|
+
target: postCycleTarget,
|
|
91
|
+
iteration: thread.iteration_count,
|
|
92
|
+
reason: 'no_new_critique_artifacts',
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
// Cap check. If incrementing iteration_count would exceed the cap,
|
|
96
|
+
// exit via max_iterations. (iteration_count is 0-indexed; cap=3 means
|
|
97
|
+
// iterations 0, 1, 2 are allowed; refusing iteration 3 → exit.)
|
|
98
|
+
if (thread.iteration_count + 1 >= iterationBlock.max_iterations) {
|
|
99
|
+
return {
|
|
100
|
+
kind: 'max_iterations',
|
|
101
|
+
target: postCycleTarget,
|
|
102
|
+
iteration: thread.iteration_count + 1,
|
|
103
|
+
max: iterationBlock.max_iterations,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
// Otherwise iterate: cycle back to the first phase of the cycle and
|
|
107
|
+
// bump iteration_count.
|
|
108
|
+
return {
|
|
109
|
+
kind: 'iterate_to',
|
|
110
|
+
target: cycle[0],
|
|
111
|
+
iteration: thread.iteration_count + 1,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Returns the artifacts produced in a specific iteration window. If an
|
|
116
|
+
* artifact has no `iteration` field (legacy, non-iterating loops, or
|
|
117
|
+
* pre-phase-2.b data), it is treated as belonging to iteration 0 so
|
|
118
|
+
* existing review_loop usage is unaffected.
|
|
119
|
+
*/
|
|
120
|
+
export function artifactsInIteration(thread, iteration) {
|
|
121
|
+
return thread.artifacts.filter((a) => (a.iteration ?? 0) === iteration);
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* `exit_when='no_new_critique_artifacts'` predicate: true when the
|
|
125
|
+
* just-completed iteration produced no critique-typed artifacts. Used
|
|
126
|
+
* by `decideNextPhase` at the cycle boundary.
|
|
127
|
+
*/
|
|
128
|
+
export function noNewCritiqueInIteration(thread, iteration) {
|
|
129
|
+
return !thread.artifacts.some((a) => (a.iteration ?? 0) === iteration && a.type === 'critique');
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* `exit_when='critic_signal'` predicate: true when the iteration
|
|
133
|
+
* contains a `type='critic_signal'` artifact (any subtype/body). The
|
|
134
|
+
* critic emits this when it judges the proposal sufficient.
|
|
135
|
+
*/
|
|
136
|
+
export function hasCriticSignalInIteration(thread, iteration) {
|
|
137
|
+
return thread.artifacts.some((a) => (a.iteration ?? 0) === iteration && a.type === 'critic_signal');
|
|
138
|
+
}
|
|
139
|
+
//# sourceMappingURL=iteration-engine.js.map
|