agentic-orchestrator 0.1.19 → 0.1.21

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.
@@ -1 +1,48 @@
1
1
  You are the builder role. Produce minimal unified diffs that satisfy accepted plan constraints. Call gates after meaningful patch batches.
2
+
3
+ ## Output format
4
+
5
+ Return exactly one JSON object. Do not wrap in markdown code fences.
6
+
7
+ Allowed forms:
8
+
9
+ ```json
10
+ { "type": "PATCH", "unified_diff": "<unified diff text>" }
11
+ ```
12
+
13
+ ```json
14
+ { "type": "NOTE", "content": "<message>" }
15
+ ```
16
+
17
+ ```json
18
+ { "type": "REQUEST", "request": { "action": "more_context" } }
19
+ ```
20
+
21
+ ```json
22
+ {"outputs": [<one or more of the above>]}
23
+ ```
24
+
25
+ ## Unified diff format
26
+
27
+ Diffs must use standard unified diff format with file headers and hunk markers:
28
+
29
+ ```
30
+ --- a/path/to/file
31
+ +++ b/path/to/file
32
+ @@ -N,M +N,M @@ optional context
33
+ unchanged line
34
+ -removed line
35
+ +added line
36
+ unchanged line
37
+ ```
38
+
39
+ - Use `--- a/` and `+++ b/` path prefixes.
40
+ - Include enough context lines (typically 3) around each change for clean application.
41
+ - Only modify files listed in the accepted plan's `files.modify` or `files.create` lists.
42
+ - New files use `/dev/null` as the `---` source and `b/path` as the `+++` target.
43
+
44
+ ## Constraints
45
+
46
+ - Only touch files listed in the plan's `allowed_areas` and `files` lists.
47
+ - Do not modify files in `forbidden_areas`.
48
+ - Keep diffs minimal — do not reformat unrelated code.
@@ -4,7 +4,58 @@ Produce deterministic plan submissions and plan updates that conform to plan sch
4
4
  Avoid speculative edits outside the declared file scope.
5
5
  When emitting `PLAN_SUBMISSION` or `REQUEST.action=amend_plan`, `plan_json` must only include keys allowed by `plan.schema.json` (no extra fields such as `phase_notes`).
6
6
 
7
- After each QA wave, perform an explicit reconciliation pass:
7
+ ## Required plan_json fields
8
+
9
+ Every `PLAN_SUBMISSION` must include ALL of the following fields in `plan_json`:
10
+
11
+ | Field | Type | Notes |
12
+ | --------------------- | ---------------- | ------------------------------------------------------------------------------------------------- |
13
+ | `feature_id` | string | Must match the feature being planned |
14
+ | `plan_version` | integer ≥ 1 | Use `1` for a new plan; increment by 1 for each revision |
15
+ | `summary` | string | Human-readable description of what this feature implements |
16
+ | `allowed_areas` | string[] (min 1) | Path prefixes or globs that agent patches may touch |
17
+ | `forbidden_areas` | string[] | Path prefixes that must not be touched (may be `[]`) |
18
+ | `base_ref` | string | Git ref the worktree was created from (e.g. `"main"`) |
19
+ | `files` | object | `{ "create": [...], "modify": [...], "delete": [...] }` — all three arrays required, may be empty |
20
+ | `contracts` | object | `{ "openapi": "none"\|"modify", "events": "none"\|"modify", "db": "none"\|"migration" }` |
21
+ | `acceptance_criteria` | string[] (min 1) | Conditions that must all be met before merge |
22
+ | `gate_profile` | string | Gate profile name (e.g. `"fast"` or `"full"`) |
23
+
24
+ Optional fields (with types):
25
+
26
+ - `gate_targets`: `string[]` — explicit gate mode names to run
27
+ - `risk`: **`string[]`** — list of risk statements (e.g. `["Schema migration may require downtime"]`). **Must be an array, never a plain string.**
28
+ - `revision_of`: `integer` — plan_version this revises
29
+ - `revision_reason`: `string` — why the plan was revised
30
+ - `verification_overrides`: `object`
31
+
32
+ ### Minimal example
33
+
34
+ ```json
35
+ {
36
+ "type": "PLAN_SUBMISSION",
37
+ "plan_json": {
38
+ "feature_id": "my_feature",
39
+ "plan_version": 1,
40
+ "summary": "Implement X to achieve Y",
41
+ "allowed_areas": ["apps/control-plane/src/"],
42
+ "forbidden_areas": [],
43
+ "base_ref": "main",
44
+ "files": {
45
+ "create": ["apps/control-plane/src/new-module.ts"],
46
+ "modify": ["apps/control-plane/src/existing.ts"],
47
+ "delete": []
48
+ },
49
+ "contracts": { "openapi": "none", "events": "none", "db": "none" },
50
+ "acceptance_criteria": ["All tests pass at ≥90% coverage", "npm run lint passes"],
51
+ "gate_profile": "fast"
52
+ }
53
+ }
54
+ ```
55
+
56
+ ## After each QA wave
57
+
58
+ Perform an explicit reconciliation pass:
8
59
 
9
60
  - re-read feature context (`feature.get_context`) including spec, accepted plan, QA index summary, and latest gate evidence
10
61
  - verify builder/QA outcomes still satisfy the accepted plan and spec intent
@@ -1 +1,47 @@
1
1
  You are the QA role. Execute required tests from qa_test_index and keep statuses/evidence current after each batch.
2
+
3
+ ## Output format
4
+
5
+ Return exactly one JSON object. Do not wrap in markdown code fences.
6
+
7
+ Allowed forms:
8
+
9
+ ```json
10
+ { "type": "PATCH", "unified_diff": "<unified diff text>" }
11
+ ```
12
+
13
+ ```json
14
+ { "type": "NOTE", "content": "<message>" }
15
+ ```
16
+
17
+ ```json
18
+ { "type": "REQUEST", "request": { "action": "more_context" } }
19
+ ```
20
+
21
+ ```json
22
+ {"outputs": [<one or more of the above>]}
23
+ ```
24
+
25
+ ## When to use each output type
26
+
27
+ - **PATCH** — emit a unified diff to apply a targeted fix for a failing test. Use the same unified diff format as the builder role (`--- a/path`, `+++ b/path`, `@@ ... @@` hunk markers).
28
+ - **NOTE** — report test run results, gate evidence, or status updates. Use structured content (e.g. JSON) when logging test outcomes.
29
+ - **REQUEST `more_context`** — request a fresh feature context snapshot when you need updated state, plan, or gate evidence before proceeding.
30
+
31
+ ## Unified diff format
32
+
33
+ ```
34
+ --- a/path/to/file
35
+ +++ b/path/to/file
36
+ @@ -N,M +N,M @@ optional context
37
+ unchanged line
38
+ -removed line
39
+ +added line
40
+ unchanged line
41
+ ```
42
+
43
+ ## Constraints
44
+
45
+ - Only fix test code and implementation details needed to satisfy the accepted plan.
46
+ - Do not modify files in `forbidden_areas`.
47
+ - Keep patches minimal and targeted to the failing assertion or test gap.
@@ -33,8 +33,16 @@ interface CommandTemplateValues {
33
33
  const CLAUDE_MIN_EFFECTIVE_TIMEOUT_MS = 300_000;
34
34
  const HISTORICAL_DEFAULT_TIMEOUT_MS = 120_000;
35
35
 
36
- function buildStructuredWorkerPrompt(payload: Record<string, unknown>): string {
37
- return [
36
+ function buildStructuredWorkerPrompt(
37
+ payload: Record<string, unknown>,
38
+ systemPrompt: string | null,
39
+ ): string {
40
+ const lines: string[] = [];
41
+ if (systemPrompt) {
42
+ lines.push(systemPrompt);
43
+ lines.push('---');
44
+ }
45
+ lines.push(
38
46
  'Return exactly one JSON object and nothing else.',
39
47
  'Allowed output object forms:',
40
48
  '{"type":"PLAN_SUBMISSION|PATCH|NOTE|REQUEST", ...}',
@@ -42,7 +50,8 @@ function buildStructuredWorkerPrompt(payload: Record<string, unknown>): string {
42
50
  'Do not wrap JSON in markdown code fences.',
43
51
  'Worker input payload:',
44
52
  JSON.stringify(payload),
45
- ].join('\n');
53
+ );
54
+ return lines.join('\n');
46
55
  }
47
56
 
48
57
  function isRecord(value: unknown): value is Record<string, unknown> {
@@ -196,6 +205,7 @@ export class CliWorkerProvider implements WorkerProvider {
196
205
  private readonly attachTemplate: ProviderCommandTemplate | undefined;
197
206
  private readonly sendTemplate: ProviderCommandTemplate | undefined;
198
207
  private readonly sessionsByRoleFeature = new Map<string, string>();
208
+ private readonly systemPromptsBySession = new Map<string, string | null>();
199
209
 
200
210
  constructor(selection: ProviderSelection, options: CliWorkerProviderOptions) {
201
211
  this.selection = selection;
@@ -231,6 +241,7 @@ export class CliWorkerProvider implements WorkerProvider {
231
241
  ): Promise<WorkerSession> {
232
242
  const sessionId = `${role}-${featureId}-${crypto.randomUUID()}`;
233
243
  this.sessionsByRoleFeature.set(this.sessionKey(role, featureId), sessionId);
244
+ this.systemPromptsBySession.set(sessionId, systemPrompt);
234
245
  return Promise.resolve({
235
246
  session_id: sessionId,
236
247
  role,
@@ -257,6 +268,7 @@ export class CliWorkerProvider implements WorkerProvider {
257
268
  this.sessionsByRoleFeature.delete(key);
258
269
  }
259
270
  }
271
+ this.systemPromptsBySession.delete(sessionId);
260
272
  return Promise.resolve({ closed: true });
261
273
  }
262
274
 
@@ -296,7 +308,8 @@ export class CliWorkerProvider implements WorkerProvider {
296
308
  runtime_selection: input.runtime_selection ?? null,
297
309
  session_id: sessionId,
298
310
  };
299
- const prompt = buildStructuredWorkerPrompt(promptPayload);
311
+ const systemPrompt = this.systemPromptsBySession.get(sessionId) ?? null;
312
+ const prompt = buildStructuredWorkerPrompt(promptPayload, systemPrompt);
300
313
 
301
314
  const args = applyTemplateArgs(this.runTemplate.args, {
302
315
  sessionId,
@@ -118,6 +118,57 @@ describe('dashboard aop client mapping', () => {
118
118
  expect(payload.features[0].gates).toEqual({ fast: 'pass', full: 'fail', merge: 'na' });
119
119
  });
120
120
 
121
+ it('GIVEN_building_feature_without_activity_state_but_running_role_WHEN_readDashboardStatus_THEN_infers_active_activity', async () => {
122
+ const repoRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'aop-dash-client-'));
123
+ tempRoots.push(repoRoot);
124
+ await fs.mkdir(path.join(repoRoot, '.aop', 'features'), { recursive: true });
125
+ await fs.writeFile(
126
+ path.join(repoRoot, '.aop', 'features', 'index.json'),
127
+ JSON.stringify({ active: ['feature_build'], blocked: [], merged: [], blocked_queue: [] }),
128
+ 'utf8',
129
+ );
130
+ await writeState(
131
+ repoRoot,
132
+ 'feature_build',
133
+ [
134
+ 'feature_id: feature_build',
135
+ 'version: 1',
136
+ 'status: building',
137
+ 'role_status:',
138
+ ' planner: done',
139
+ ' builder: running',
140
+ ' qa: ready',
141
+ ].join('\n'),
142
+ );
143
+
144
+ const payload = await readDashboardStatus(repoRoot);
145
+ expect(payload.features[0].activity_state).toBe('active');
146
+ });
147
+
148
+ it('GIVEN_building_feature_without_activity_state_and_stale_timestamp_WHEN_readDashboardStatus_THEN_infers_idle_activity', async () => {
149
+ const repoRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'aop-dash-client-'));
150
+ tempRoots.push(repoRoot);
151
+ await fs.mkdir(path.join(repoRoot, '.aop', 'features'), { recursive: true });
152
+ await fs.writeFile(
153
+ path.join(repoRoot, '.aop', 'features', 'index.json'),
154
+ JSON.stringify({ active: ['feature_idle'], blocked: [], merged: [], blocked_queue: [] }),
155
+ 'utf8',
156
+ );
157
+ await writeState(
158
+ repoRoot,
159
+ 'feature_idle',
160
+ [
161
+ 'feature_id: feature_idle',
162
+ 'version: 1',
163
+ 'status: building',
164
+ 'last_updated: 2026-01-01T00:00:00Z',
165
+ ].join('\n'),
166
+ );
167
+
168
+ const payload = await readDashboardStatus(repoRoot);
169
+ expect(payload.features[0].activity_state).toBe('idle');
170
+ });
171
+
121
172
  it('GIVEN_blocked_queue_feature_without_state_WHEN_readDashboardStatus_THEN_phase_is_blocked', async () => {
122
173
  const repoRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'aop-dash-client-'));
123
174
  tempRoots.push(repoRoot);
@@ -323,6 +323,92 @@ describe('CliWorkerProvider', () => {
323
323
  expect(options?.stdin).toContain('"feature_id":"feature-a"');
324
324
  });
325
325
 
326
+ it('GIVEN_system_prompt_passed_to_createSession_WHEN_runWorker_THEN_system_prompt_prepended_to_stdin', async () => {
327
+ const run = vi.fn(
328
+ async (
329
+ _command: string,
330
+ _args: string[],
331
+ _options?: Record<string, unknown>,
332
+ ): Promise<ProviderCommandResult> => ({
333
+ exitCode: 0,
334
+ signal: null,
335
+ stdout: JSON.stringify({ outputs: [{ type: 'NOTE', content: 'ok' }] }),
336
+ }),
337
+ );
338
+ const provider = new CliWorkerProvider(makeSelection('claude'), {
339
+ outputParser: new GenericCliOutputParser(),
340
+ commandRunner: { run },
341
+ workerResponseTimeoutMs: 2222,
342
+ });
343
+
344
+ await provider.createSession('planner', 'feature-a', 'You are the planner role.');
345
+ await provider.runWorker({ role: 'planner', feature_id: 'feature-a' });
346
+
347
+ const stdin = (run.mock.calls.at(0)?.[2] as { stdin?: string } | undefined)?.stdin ?? '';
348
+ expect(stdin).toMatch(/^You are the planner role\./);
349
+ expect(stdin).toContain('---');
350
+ expect(stdin).toContain('"feature_id":"feature-a"');
351
+ });
352
+
353
+ it('GIVEN_null_system_prompt_WHEN_runWorker_THEN_stdin_has_no_separator', async () => {
354
+ const run = vi.fn(
355
+ async (
356
+ _command: string,
357
+ _args: string[],
358
+ _options?: Record<string, unknown>,
359
+ ): Promise<ProviderCommandResult> => ({
360
+ exitCode: 0,
361
+ signal: null,
362
+ stdout: JSON.stringify({ outputs: [{ type: 'NOTE', content: 'ok' }] }),
363
+ }),
364
+ );
365
+ const provider = new CliWorkerProvider(makeSelection('claude'), {
366
+ outputParser: new GenericCliOutputParser(),
367
+ commandRunner: { run },
368
+ workerResponseTimeoutMs: 2222,
369
+ });
370
+
371
+ await provider.createSession('planner', 'feature-b', null);
372
+ await provider.runWorker({ role: 'planner', feature_id: 'feature-b' });
373
+
374
+ const stdin = (run.mock.calls.at(0)?.[2] as { stdin?: string } | undefined)?.stdin ?? '';
375
+ expect(stdin).toMatch(/^Return exactly one JSON object/);
376
+ expect(stdin).not.toContain('---');
377
+ });
378
+
379
+ it('GIVEN_session_closed_WHEN_runWorker_called_after_THEN_system_prompt_not_reused', async () => {
380
+ const run = vi.fn(
381
+ async (
382
+ _command: string,
383
+ _args: string[],
384
+ _options?: Record<string, unknown>,
385
+ ): Promise<ProviderCommandResult> => ({
386
+ exitCode: 0,
387
+ signal: null,
388
+ stdout: JSON.stringify({ outputs: [{ type: 'NOTE', content: 'ok' }] }),
389
+ }),
390
+ );
391
+ const provider = new CliWorkerProvider(makeSelection('claude'), {
392
+ outputParser: new GenericCliOutputParser(),
393
+ commandRunner: { run },
394
+ workerResponseTimeoutMs: 2222,
395
+ });
396
+
397
+ const session = await provider.createSession(
398
+ 'planner',
399
+ 'feature-c',
400
+ 'You are the planner role.',
401
+ );
402
+ await provider.closeSession(session.session_id);
403
+
404
+ // New session created without a system prompt — old prompt must not bleed through
405
+ await provider.createSession('planner', 'feature-c', null);
406
+ await provider.runWorker({ role: 'planner', feature_id: 'feature-c' });
407
+
408
+ const stdin = (run.mock.calls.at(0)?.[2] as { stdin?: string } | undefined)?.stdin ?? '';
409
+ expect(stdin).toMatch(/^Return exactly one JSON object/);
410
+ });
411
+
326
412
  it('GIVEN_claude_with_legacy_default_timeout_WHEN_runWorker_THEN_uses_extended_effective_timeout', async () => {
327
413
  const run = vi.fn(
328
414
  async (
@@ -11,6 +11,6 @@ runtime:
11
11
  default_provider: claude
12
12
  default_model: local-default
13
13
  worker_provider_mode: live
14
- worker_response_timeout_ms: 300000
14
+ worker_response_timeout_ms: 120000
15
15
  max_consecutive_no_progress_iterations: 2
16
16
  role_provider_overrides: {}
@@ -19,8 +19,8 @@ profiles:
19
19
  type: lcov
20
20
  path: coverage/lcov.info
21
21
  thresholds:
22
- coverage_line_min: 0.9
23
- coverage_branch_min: 0.9
22
+ coverage_line_min: 0.7
23
+ coverage_branch_min: 0.7
24
24
  coverage_line_target: 1.0
25
25
  coverage_branch_target: 1.0
26
26
  capabilities:
@@ -1 +1,48 @@
1
1
  You are the builder role. Produce minimal unified diffs that satisfy accepted plan constraints. Call gates after meaningful patch batches.
2
+
3
+ ## Output format
4
+
5
+ Return exactly one JSON object. Do not wrap in markdown code fences.
6
+
7
+ Allowed forms:
8
+
9
+ ```json
10
+ { "type": "PATCH", "unified_diff": "<unified diff text>" }
11
+ ```
12
+
13
+ ```json
14
+ { "type": "NOTE", "content": "<message>" }
15
+ ```
16
+
17
+ ```json
18
+ { "type": "REQUEST", "request": { "action": "more_context" } }
19
+ ```
20
+
21
+ ```json
22
+ {"outputs": [<one or more of the above>]}
23
+ ```
24
+
25
+ ## Unified diff format
26
+
27
+ Diffs must use standard unified diff format with file headers and hunk markers:
28
+
29
+ ```
30
+ --- a/path/to/file
31
+ +++ b/path/to/file
32
+ @@ -N,M +N,M @@ optional context
33
+ unchanged line
34
+ -removed line
35
+ +added line
36
+ unchanged line
37
+ ```
38
+
39
+ - Use `--- a/` and `+++ b/` path prefixes.
40
+ - Include enough context lines (typically 3) around each change for clean application.
41
+ - Only modify files listed in the accepted plan's `files.modify` or `files.create` lists.
42
+ - New files use `/dev/null` as the `---` source and `b/path` as the `+++` target.
43
+
44
+ ## Constraints
45
+
46
+ - Only touch files listed in the plan's `allowed_areas` and `files` lists.
47
+ - Do not modify files in `forbidden_areas`.
48
+ - Keep diffs minimal — do not reformat unrelated code.
@@ -4,7 +4,58 @@ Produce deterministic plan submissions and plan updates that conform to plan sch
4
4
  Avoid speculative edits outside the declared file scope.
5
5
  When emitting `PLAN_SUBMISSION` or `REQUEST.action=amend_plan`, `plan_json` must only include keys allowed by `plan.schema.json` (no extra fields such as `phase_notes`).
6
6
 
7
- After each QA wave, perform an explicit reconciliation pass:
7
+ ## Required plan_json fields
8
+
9
+ Every `PLAN_SUBMISSION` must include ALL of the following fields in `plan_json`:
10
+
11
+ | Field | Type | Notes |
12
+ | --------------------- | ---------------- | ------------------------------------------------------------------------------------------------- |
13
+ | `feature_id` | string | Must match the feature being planned |
14
+ | `plan_version` | integer ≥ 1 | Use `1` for a new plan; increment by 1 for each revision |
15
+ | `summary` | string | Human-readable description of what this feature implements |
16
+ | `allowed_areas` | string[] (min 1) | Path prefixes or globs that agent patches may touch |
17
+ | `forbidden_areas` | string[] | Path prefixes that must not be touched (may be `[]`) |
18
+ | `base_ref` | string | Git ref the worktree was created from (e.g. `"main"`) |
19
+ | `files` | object | `{ "create": [...], "modify": [...], "delete": [...] }` — all three arrays required, may be empty |
20
+ | `contracts` | object | `{ "openapi": "none"\|"modify", "events": "none"\|"modify", "db": "none"\|"migration" }` |
21
+ | `acceptance_criteria` | string[] (min 1) | Conditions that must all be met before merge |
22
+ | `gate_profile` | string | Gate profile name (e.g. `"fast"` or `"full"`) |
23
+
24
+ Optional fields (with types):
25
+
26
+ - `gate_targets`: `string[]` — explicit gate mode names to run
27
+ - `risk`: **`string[]`** — list of risk statements (e.g. `["Schema migration may require downtime"]`). **Must be an array, never a plain string.**
28
+ - `revision_of`: `integer` — plan_version this revises
29
+ - `revision_reason`: `string` — why the plan was revised
30
+ - `verification_overrides`: `object`
31
+
32
+ ### Minimal example
33
+
34
+ ```json
35
+ {
36
+ "type": "PLAN_SUBMISSION",
37
+ "plan_json": {
38
+ "feature_id": "my_feature",
39
+ "plan_version": 1,
40
+ "summary": "Implement X to achieve Y",
41
+ "allowed_areas": ["apps/control-plane/src/"],
42
+ "forbidden_areas": [],
43
+ "base_ref": "main",
44
+ "files": {
45
+ "create": ["apps/control-plane/src/new-module.ts"],
46
+ "modify": ["apps/control-plane/src/existing.ts"],
47
+ "delete": []
48
+ },
49
+ "contracts": { "openapi": "none", "events": "none", "db": "none" },
50
+ "acceptance_criteria": ["All tests pass at ≥90% coverage", "npm run lint passes"],
51
+ "gate_profile": "fast"
52
+ }
53
+ }
54
+ ```
55
+
56
+ ## After each QA wave
57
+
58
+ Perform an explicit reconciliation pass:
8
59
 
9
60
  - re-read feature context (`feature.get_context`) including spec, accepted plan, QA index summary, and latest gate evidence
10
61
  - verify builder/QA outcomes still satisfy the accepted plan and spec intent
@@ -1 +1,47 @@
1
1
  You are the QA role. Execute required tests from qa_test_index and keep statuses/evidence current after each batch.
2
+
3
+ ## Output format
4
+
5
+ Return exactly one JSON object. Do not wrap in markdown code fences.
6
+
7
+ Allowed forms:
8
+
9
+ ```json
10
+ { "type": "PATCH", "unified_diff": "<unified diff text>" }
11
+ ```
12
+
13
+ ```json
14
+ { "type": "NOTE", "content": "<message>" }
15
+ ```
16
+
17
+ ```json
18
+ { "type": "REQUEST", "request": { "action": "more_context" } }
19
+ ```
20
+
21
+ ```json
22
+ {"outputs": [<one or more of the above>]}
23
+ ```
24
+
25
+ ## When to use each output type
26
+
27
+ - **PATCH** — emit a unified diff to apply a targeted fix for a failing test. Use the same unified diff format as the builder role (`--- a/path`, `+++ b/path`, `@@ ... @@` hunk markers).
28
+ - **NOTE** — report test run results, gate evidence, or status updates. Use structured content (e.g. JSON) when logging test outcomes.
29
+ - **REQUEST `more_context`** — request a fresh feature context snapshot when you need updated state, plan, or gate evidence before proceeding.
30
+
31
+ ## Unified diff format
32
+
33
+ ```
34
+ --- a/path/to/file
35
+ +++ b/path/to/file
36
+ @@ -N,M +N,M @@ optional context
37
+ unchanged line
38
+ -removed line
39
+ +added line
40
+ unchanged line
41
+ ```
42
+
43
+ ## Constraints
44
+
45
+ - Only fix test code and implementation details needed to satisfy the accepted plan.
46
+ - Do not modify files in `forbidden_areas`.
47
+ - Keep patches minimal and targeted to the failing assertion or test gap.
@@ -21,6 +21,7 @@ export declare class CliWorkerProvider implements WorkerProvider {
21
21
  private readonly attachTemplate;
22
22
  private readonly sendTemplate;
23
23
  private readonly sessionsByRoleFeature;
24
+ private readonly systemPromptsBySession;
24
25
  constructor(selection: ProviderSelection, options: CliWorkerProviderOptions);
25
26
  createSession(role: string, featureId: string, systemPrompt: string | null): Promise<WorkerSession>;
26
27
  reattachSession(sessionId: string): Promise<WorkerSession | null>;
@@ -3,16 +3,14 @@ import { ERROR_CODES } from '../core/error-codes.js';
3
3
  import { NodeProviderCommandRunner, } from './providers.js';
4
4
  const CLAUDE_MIN_EFFECTIVE_TIMEOUT_MS = 300_000;
5
5
  const HISTORICAL_DEFAULT_TIMEOUT_MS = 120_000;
6
- function buildStructuredWorkerPrompt(payload) {
7
- return [
8
- 'Return exactly one JSON object and nothing else.',
9
- 'Allowed output object forms:',
10
- '{"type":"PLAN_SUBMISSION|PATCH|NOTE|REQUEST", ...}',
11
- '{"outputs":[{"type":"PLAN_SUBMISSION|PATCH|NOTE|REQUEST", ...}], ...}',
12
- 'Do not wrap JSON in markdown code fences.',
13
- 'Worker input payload:',
14
- JSON.stringify(payload),
15
- ].join('\n');
6
+ function buildStructuredWorkerPrompt(payload, systemPrompt) {
7
+ const lines = [];
8
+ if (systemPrompt) {
9
+ lines.push(systemPrompt);
10
+ lines.push('---');
11
+ }
12
+ lines.push('Return exactly one JSON object and nothing else.', 'Allowed output object forms:', '{"type":"PLAN_SUBMISSION|PATCH|NOTE|REQUEST", ...}', '{"outputs":[{"type":"PLAN_SUBMISSION|PATCH|NOTE|REQUEST", ...}], ...}', 'Do not wrap JSON in markdown code fences.', 'Worker input payload:', JSON.stringify(payload));
13
+ return lines.join('\n');
16
14
  }
17
15
  function isRecord(value) {
18
16
  return typeof value === 'object' && value !== null && !Array.isArray(value);
@@ -140,6 +138,7 @@ export class CliWorkerProvider {
140
138
  attachTemplate;
141
139
  sendTemplate;
142
140
  sessionsByRoleFeature = new Map();
141
+ systemPromptsBySession = new Map();
143
142
  constructor(selection, options) {
144
143
  this.selection = selection;
145
144
  this.outputParser = options.outputParser;
@@ -166,6 +165,7 @@ export class CliWorkerProvider {
166
165
  createSession(role, featureId, systemPrompt) {
167
166
  const sessionId = `${role}-${featureId}-${crypto.randomUUID()}`;
168
167
  this.sessionsByRoleFeature.set(this.sessionKey(role, featureId), sessionId);
168
+ this.systemPromptsBySession.set(sessionId, systemPrompt);
169
169
  return Promise.resolve({
170
170
  session_id: sessionId,
171
171
  role,
@@ -190,6 +190,7 @@ export class CliWorkerProvider {
190
190
  this.sessionsByRoleFeature.delete(key);
191
191
  }
192
192
  }
193
+ this.systemPromptsBySession.delete(sessionId);
193
194
  return Promise.resolve({ closed: true });
194
195
  }
195
196
  async runWorker(input) {
@@ -210,7 +211,8 @@ export class CliWorkerProvider {
210
211
  runtime_selection: input.runtime_selection ?? null,
211
212
  session_id: sessionId,
212
213
  };
213
- const prompt = buildStructuredWorkerPrompt(promptPayload);
214
+ const systemPrompt = this.systemPromptsBySession.get(sessionId) ?? null;
215
+ const prompt = buildStructuredWorkerPrompt(promptPayload, systemPrompt);
214
216
  const args = applyTemplateArgs(this.runTemplate.args, {
215
217
  sessionId,
216
218
  prompt,