@yeaft/webchat-agent 1.0.143 → 1.0.145

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yeaft/webchat-agent",
3
- "version": "1.0.143",
3
+ "version": "1.0.145",
4
4
  "description": "Remote worker agent for Yeaft Web Code Agent — connects the native Yeaft engine, CLI providers, and workbench tools",
5
5
  "main": "index.js",
6
6
  "type": "module",
package/yeaft/engine.js CHANGED
@@ -1008,12 +1008,13 @@ export class Engine {
1008
1008
  * @param {object} [args.vpPersona]
1009
1009
  * @param {object} [args.activeScope] — DESIGN-PROMPT §3 ④ structured scope summary
1010
1010
  * @param {string} [args.sessionAnnouncement]
1011
+ * @param {string} [args.workCenterInstructions] — frozen Agent-level Work Center policy
1011
1012
  * @param {string} [args.projectDoc] — resolved CLAUDE.md / AGENTS.md text (already truncated)
1012
1013
  * @param {object} [args.taskCtx] — legacy task-context sub-block (optional)
1013
1014
  * @param {string} [args.explicitSkillName] — leading /skill:<name> command, if present
1014
1015
  * @returns {string}
1015
1016
  */
1016
- #buildSystemPrompt({ prompt, memoryInjection, vpPersona, activeScope, sessionAnnouncement, projectDoc, taskCtx, activeTasks, explicitSkillName } = {}) {
1017
+ #buildSystemPrompt({ prompt, memoryInjection, vpPersona, activeScope, sessionAnnouncement, workCenterInstructions, projectDoc, taskCtx, activeTasks, explicitSkillName } = {}) {
1017
1018
  // Get relevant skill content if SkillManager is wired. A leading
1018
1019
  // /skill:<name> is explicit, not relevance matching: load that skill by
1019
1020
  // name or inject a visible prompt warning when the command is unknown.
@@ -1040,6 +1041,7 @@ export class Engine {
1040
1041
  vpPersona,
1041
1042
  activeScope,
1042
1043
  sessionAnnouncement,
1044
+ workCenterInstructions,
1043
1045
  projectDoc,
1044
1046
  runtimePlatform: getRuntimePlatformInfo(),
1045
1047
  taskCtx,
@@ -1647,7 +1649,7 @@ export class Engine {
1647
1649
  * string-prompt shape (no regression for existing callers).
1648
1650
  * @yields {EngineEvent}
1649
1651
  */
1650
- async *query({ prompt, promptParts = null, messages = [], signal, userEffort = null, scenario = 'chat', vpPersona, router, senderVpId, inboundEnvelope, taskId, taskMembers, sessionId, sessionMembers, sessionTopics = null, vpPlan, sessionAnnouncement, workDir, userAlreadyPersisted = false, getCurrentTodos = null, setCurrentTodos = null, threadId = MAIN_THREAD_ID, vpTurnId = null, drainPendingUserMessages = null, collabToolPolicy = null } = {}) {
1652
+ async *query({ prompt, promptParts = null, messages = [], signal, userEffort = null, scenario = 'chat', vpPersona, router, senderVpId, inboundEnvelope, taskId, taskMembers, sessionId, sessionMembers, sessionTopics = null, vpPlan, sessionAnnouncement, workCenterInstructions, workDir, userAlreadyPersisted = false, getCurrentTodos = null, setCurrentTodos = null, threadId = MAIN_THREAD_ID, vpTurnId = null, drainPendingUserMessages = null, collabToolPolicy = null } = {}) {
1651
1653
  if (!prompt || typeof prompt !== 'string' || !prompt.trim()) {
1652
1654
  yield {
1653
1655
  type: 'error',
@@ -1715,7 +1717,7 @@ export class Engine {
1715
1717
 
1716
1718
  try {
1717
1719
  this.#currentThreadId = threadId || MAIN_THREAD_ID;
1718
- yield* this.#runQuery({ prompt: effectivePrompt, promptParts: effectivePromptParts, messages, signal: runSignal, userEffort: effectiveUserEffort, scenario, vpPersona, router, senderVpId, inboundEnvelope, taskId, taskMembers, sessionId, sessionMembers, sessionTopics, vpPlan, sessionAnnouncement, workDir, userAlreadyPersisted, getCurrentTodos, setCurrentTodos, threadId: this.#currentThreadId, vpTurnId, drainPendingUserMessages, collabToolPolicy: effectiveCollabToolPolicy, explicitSkillName: parsedSkill.skillName });
1720
+ yield* this.#runQuery({ prompt: effectivePrompt, promptParts: effectivePromptParts, messages, signal: runSignal, userEffort: effectiveUserEffort, scenario, vpPersona, router, senderVpId, inboundEnvelope, taskId, taskMembers, sessionId, sessionMembers, sessionTopics, vpPlan, sessionAnnouncement, workCenterInstructions, workDir, userAlreadyPersisted, getCurrentTodos, setCurrentTodos, threadId: this.#currentThreadId, vpTurnId, drainPendingUserMessages, collabToolPolicy: effectiveCollabToolPolicy, explicitSkillName: parsedSkill.skillName });
1719
1721
  } finally {
1720
1722
  if (signal) {
1721
1723
  try { signal.removeEventListener('abort', onExternalAbort); } catch { /* ignore */ }
@@ -1759,7 +1761,7 @@ export class Engine {
1759
1761
  * in a try/finally without indenting the whole loop.
1760
1762
  * @private
1761
1763
  */
1762
- async *#runQuery({ prompt, promptParts = null, messages, signal, userEffort = null, scenario = 'chat', vpPersona, router, senderVpId, inboundEnvelope, taskId, taskMembers, sessionId, sessionMembers, sessionTopics = null, vpPlan, sessionAnnouncement, workDir, userAlreadyPersisted = false, getCurrentTodos = null, setCurrentTodos = null, threadId = MAIN_THREAD_ID, vpTurnId = null, drainPendingUserMessages = null, collabToolPolicy = null, explicitSkillName = null }) {
1764
+ async *#runQuery({ prompt, promptParts = null, messages, signal, userEffort = null, scenario = 'chat', vpPersona, router, senderVpId, inboundEnvelope, taskId, taskMembers, sessionId, sessionMembers, sessionTopics = null, vpPlan, sessionAnnouncement, workCenterInstructions, workDir, userAlreadyPersisted = false, getCurrentTodos = null, setCurrentTodos = null, threadId = MAIN_THREAD_ID, vpTurnId = null, drainPendingUserMessages = null, collabToolPolicy = null, explicitSkillName = null }) {
1763
1765
 
1764
1766
  const effectiveCollabToolPolicy = collabToolPolicy === COLLAB_TOOL_POLICY.SINGLE_VP || collabToolPolicy === COLLAB_TOOL_POLICY.MULTI_VP
1765
1767
  ? collabToolPolicy
@@ -1896,6 +1898,7 @@ export class Engine {
1896
1898
  vpPersona,
1897
1899
  activeScope,
1898
1900
  sessionAnnouncement,
1901
+ workCenterInstructions,
1899
1902
  projectDoc,
1900
1903
  activeTasks,
1901
1904
  explicitSkillName,
package/yeaft/prompts.js CHANGED
@@ -204,6 +204,8 @@ const PROMPTS = {
204
204
  activeScopeEnvelopeLabel: 'Handoff',
205
205
  multiVpRoutingHeader: '## multi_vp_routing',
206
206
  sessionAnnouncementHeader: '[Session Announcement]',
207
+ workCenterInstructionsHeader: '[Work Center Agent Instructions]',
208
+ workCenterInstructionsIntro: 'These Agent-level instructions apply to every Action in this WorkItem. Follow them unless they conflict with system/tool safety rules, the authoritative project document, or the WorkItem contract.',
207
209
  // Project-doc (CLAUDE.md / AGENTS.md) header + one-liner intro. Both
208
210
  // filenames are recognized: CLAUDE.md is this project's convention,
209
211
  // AGENTS.md is the cross-tool convention (Codex / OpenAI Codex CLI).
@@ -224,6 +226,8 @@ const PROMPTS = {
224
226
  activeScopeEnvelopeLabel: '转交消息',
225
227
  multiVpRoutingHeader: '## multi_vp_routing',
226
228
  sessionAnnouncementHeader: '[会话公告]',
229
+ workCenterInstructionsHeader: '[Work Center Agent 指令]',
230
+ workCenterInstructionsIntro: '这些 Agent 级指令作用于当前 Work Item 的每个 Action。除非与系统/工具安全规则、权威项目文档或 Work Item 契约冲突,否则必须遵循。',
227
231
  // 项目文档块:CLAUDE.md / AGENTS.md(与 Codex 通用命名兼容)。
228
232
  projectDocHeader: '[项目文档]',
229
233
  projectDocIntro:
@@ -293,6 +297,7 @@ export function normalizePromptLanguage(language) {
293
297
  * activeScope?: object,
294
298
  * vpPersona?: object,
295
299
  * sessionAnnouncement?: string,
300
+ * workCenterInstructions?: string,
296
301
  * projectDoc?: string,
297
302
  * }} params
298
303
  * @returns {string}
@@ -306,6 +311,7 @@ export function buildSystemPrompt({
306
311
  activeScope,
307
312
  vpPersona,
308
313
  sessionAnnouncement = '',
314
+ workCenterInstructions = '',
309
315
  projectDoc = '',
310
316
  runtimePlatform,
311
317
  activeTasks = '',
@@ -360,6 +366,13 @@ export function buildSystemPrompt({
360
366
  parts.push(`${lang.sessionAnnouncementHeader || '[Session Announcement]'}\n${annText}`);
361
367
  }
362
368
 
369
+ const workCenterText = (typeof workCenterInstructions === 'string') ? workCenterInstructions.trim() : '';
370
+ if (workCenterText) {
371
+ const header = lang.workCenterInstructionsHeader || '[Work Center Agent Instructions]';
372
+ const intro = lang.workCenterInstructionsIntro || '';
373
+ parts.push(`${header}\n${intro ? `${intro}\n\n` : ''}${workCenterText}`);
374
+ }
375
+
363
376
  // ─── 2. Date Metadata ──────────────────────────────────
364
377
  parts.push(lang.date(new Date().toISOString().split('T')[0]));
365
378
 
@@ -34,7 +34,7 @@ Use this when work must continue beyond the current turn, needs role handoffs, r
34
34
  },
35
35
  workDir: {
36
36
  type: 'string',
37
- description: { en: 'Optional project directory for execution', zh: '执行时使用的可选项目目录' },
37
+ description: { en: 'Existing project directory for execution', zh: '执行时使用的已存在项目目录' },
38
38
  },
39
39
  start: {
40
40
  type: 'boolean',
@@ -14,6 +14,11 @@ const CAPABILITY_TERMS = Object.freeze({
14
14
  review: ['review', 'reviewer', 'refactor', 'architecture', 'code-smells', 'readability', 'maintainability'],
15
15
  deliver: ['deliver', 'release', 'ship', 'git', 'engineering', 'systems', 'execution'],
16
16
  research: ['research', 'science', 'analysis', 'evidence', 'investigation'],
17
+ design: ['design', 'architecture', 'architect', 'systems', 'product', 'ux'],
18
+ diagnose: ['diagnose', 'debug', 'root-cause', 'reliability', 'investigation', 'systems'],
19
+ migrate: ['migrate', 'migration', 'database', 'compatibility', 'data', 'systems'],
20
+ document: ['document', 'documentation', 'writer', 'writing', 'communication'],
21
+ operate: ['operate', 'operations', 'release', 'deployment', 'reliability', 'systems'],
17
22
  write: ['write', 'writer', 'writing', 'documentation', 'editor', 'communication'],
18
23
  });
19
24
 
@@ -34,7 +39,7 @@ function capabilityScore(vp, capability) {
34
39
  const needle = String(capability || '').trim().toLowerCase();
35
40
  if (!needle) return 0;
36
41
  const text = vpSearchText(vp);
37
- const terms = CAPABILITY_TERMS[needle] || [needle];
42
+ const terms = Object.hasOwn(CAPABILITY_TERMS, needle) ? CAPABILITY_TERMS[needle] : [needle];
38
43
  let score = 0;
39
44
  for (const term of terms) {
40
45
  if (text.includes(term)) score += term === needle ? 6 : 2;
@@ -70,6 +70,7 @@ async function getSettingsRuntime() {
70
70
  models: Array.isArray(runtime.config.availableModels) ? runtime.config.availableModels : [],
71
71
  primaryModel: runtime.config.primaryModel || runtime.config.model || null,
72
72
  fastModel: runtime.config.fastModel || null,
73
+ defaultWorkDir: ctx.CONFIG?.workDir || process.cwd(),
73
74
  defaultStageInstructions: defaultWorkCenterStageInstructions(),
74
75
  };
75
76
  }
@@ -277,7 +277,7 @@ function completionContract(action, workItem) {
277
277
  ? ',\n "contractPatch": { "goal": "optional refined goal", "acceptanceCriteria": ["optional refined criterion"] }'
278
278
  : '';
279
279
  const planField = action.type === 'triage' && workItem?.workflowSnapshot?.planningMode === 'ai'
280
- ? ',\n "plan": { "workItemType": "dynamic-type", "actions": [{ "id": "stable-id", "name": "User-facing name", "type": "implement|test|review|deliver|research|write|custom", "capability": "executor capability", "objective": "specific Action objective", "separateFromActionTypes": ["optional prior Action type"], "changesRequestedActionId": "required for review when applicable", "maxAttempts": 2 }] }'
280
+ ? ',\n "plan": { "workItemType": "specific-lowercase-slug", "actions": [{ "id": "stable-id", "name": "User-facing name", "type": "extensible-lowercase-slug (built-ins include research|design|diagnose|implement|migrate|test|review|document|operate|deliver|write|custom)", "capability": "specific executor capability", "objective": "independently executable and verifiable Action objective", "separateFromActionTypes": ["optional prior Action type"], "changesRequestedActionId": "for review: optional earlier editable Action id; omit to use nearest", "maxAttempts": 2 }] }'
281
281
  : '';
282
282
  return `\n\nYou are executing one Work Center Action. Before the terminal JSON, write a concise user-facing response describing what you did and the result. Do not include raw tool output or secrets. End your response with exactly one JSON object, preferably in a json code fence:\n{
283
283
  "outcome": "completed|waiting|retryable|failed",
@@ -452,6 +452,7 @@ export class WorkItemRunner {
452
452
  signal,
453
453
  scenario: 'work-item',
454
454
  vpPersona: personaFor(vp),
455
+ workCenterInstructions: workItem?.workflowSnapshot?.globalInstructions || '',
455
456
  workDir,
456
457
  userAlreadyPersisted: true,
457
458
  collabToolPolicy: 'single-vp',
@@ -1,4 +1,5 @@
1
- import { join } from 'node:path';
1
+ import { realpathSync, statSync } from 'node:fs';
2
+ import { join, resolve } from 'node:path';
2
3
  import { randomUUID } from 'node:crypto';
3
4
  import { WorkItemStore } from './store.js';
4
5
  import { WorkflowController } from './controller.js';
@@ -16,6 +17,18 @@ function requiredString(value, name) {
16
17
  return value.trim();
17
18
  }
18
19
 
20
+ function requiredWorkDir(value) {
21
+ const workDir = requiredString(value, 'workDir');
22
+ let canonical;
23
+ try {
24
+ canonical = realpathSync(resolve(workDir));
25
+ if (!statSync(canonical).isDirectory()) throw new Error('not a directory');
26
+ } catch {
27
+ throw new Error('workDir must be an existing directory');
28
+ }
29
+ return canonical;
30
+ }
31
+
19
32
  export class WorkCenterService {
20
33
  constructor(options) {
21
34
  const yeaftDir = requiredString(options?.yeaftDir, 'yeaftDir');
@@ -72,6 +85,12 @@ export class WorkCenterService {
72
85
  const workflowSnapshot = explicitWorkflow
73
86
  ? resolveWorkflowSnapshot(settings, explicitWorkflow, payload.stageOverrides)
74
87
  : resolvePlanningWorkflowSnapshot(settings);
88
+ const runtime = !Object.hasOwn(payload, 'workDir') && !settings.defaultWorkDir
89
+ ? await this.runtimeInfo()
90
+ : null;
91
+ const workDir = requiredWorkDir(Object.hasOwn(payload, 'workDir')
92
+ ? payload.workDir
93
+ : settings.defaultWorkDir || runtime?.defaultWorkDir);
75
94
  const item = this.controller.create({
76
95
  title: requiredString(payload.title, 'title'),
77
96
  goal: requiredString(payload.goal, 'goal'),
@@ -80,9 +99,7 @@ export class WorkCenterService {
80
99
  : [],
81
100
  workflowTemplate,
82
101
  workflowSnapshot,
83
- workDir: typeof payload.workDir === 'string' && payload.workDir.trim()
84
- ? payload.workDir.trim()
85
- : settings.defaultWorkDir,
102
+ workDir,
86
103
  reuseMemory: payload.reuseMemory !== false,
87
104
  origin: payload.origin && typeof payload.origin === 'object'
88
105
  ? {
@@ -1,17 +1,37 @@
1
- const STAGE_TYPES = new Set(['triage', 'implement', 'test', 'review', 'deliver', 'research', 'write', 'custom']);
1
+ export const BUILT_IN_ACTION_TYPES = Object.freeze([
2
+ 'triage',
3
+ 'research',
4
+ 'design',
5
+ 'diagnose',
6
+ 'implement',
7
+ 'migrate',
8
+ 'test',
9
+ 'review',
10
+ 'document',
11
+ 'operate',
12
+ 'deliver',
13
+ 'write',
14
+ 'custom',
15
+ ]);
16
+ const STAGE_TYPES = new Set(BUILT_IN_ACTION_TYPES);
2
17
  const ASSIGNMENT_MODES = new Set(['auto', 'pool', 'fixed']);
3
18
  const MODEL_MODES = new Set(['inherit', 'primary', 'fast', 'specific']);
4
19
  const MODEL_EFFORTS = new Set(['minimal', 'low', 'medium', 'high', 'xhigh', 'max']);
5
20
 
6
21
  const DEFAULT_STAGE_INSTRUCTIONS = Object.freeze({
7
- triage: 'Analyze the request, verify scope and risks, and make the contract executable. Do not implement yet. If the goal or acceptance criteria need refinement, submit a contractPatch.',
8
- implement: 'Implement the smallest correct change in the supplied work directory. Add and run relevant tests. Use the prior triage/review findings. Do not approve your own work.',
9
- test: 'Verify the implementation against the acceptance criteria. Run focused tests and report reproducible evidence. Do not modify unrelated code.',
10
- review: 'Review the implementation and evidence independently. Return approved or changes_requested with concrete findings.',
11
- deliver: 'Deliver the approved change using the repository release policy. Verify the final remote state and provide evidence.',
12
- research: 'Research the question using verifiable sources. Separate evidence from inference and return a concise synthesis.',
13
- write: 'Produce the requested written deliverable, then verify it against every acceptance criterion.',
14
- custom: 'Complete this stage and return verifiable evidence.',
22
+ triage: 'Turn the request into an executable contract. Inspect relevant repository facts before deciding the flow. Classify the WorkItem, identify constraints, risks, dependencies, and missing acceptance criteria, then plan only the Actions needed for this task. Do not implement. If the goal or acceptance criteria must change, submit a contractPatch and explain why.',
23
+ research: 'Answer the Action objective with verifiable evidence. Search the repository and, when needed, authoritative external sources. Separate observed facts from inference, record unresolved uncertainty, and produce a concise conclusion that a later Action can use.',
24
+ design: 'Design the smallest maintainable solution that satisfies the WorkItem contract. Inspect existing architecture and conventions, define data flow and boundaries, consider failure and compatibility cases, and leave concrete implementation guidance. Do not add abstraction without a demonstrated need.',
25
+ diagnose: 'Reproduce or otherwise establish the reported behavior, trace it to a root cause, and distinguish evidence from hypotheses. Identify the smallest safe correction and the regression tests that would prove it. Do not stop at the visible symptom.',
26
+ implement: 'Implement the Action objective in the supplied work directory using the smallest correct diff. Follow repository conventions, preserve compatibility, handle relevant boundary conditions, and add or update focused tests. Run the checks needed to prove the change. Use prior Action and review findings; do not approve your own work.',
27
+ migrate: 'Execute the required migration without losing existing semantics or data. Define compatibility and rollback boundaries, make the operation repeatable or safely fenced, validate representative legacy data, and provide evidence for both upgraded and already-current states.',
28
+ test: 'Verify the current result against every applicable acceptance criterion. Run focused tests first, then the broader checks justified by the risk. Cover failure, compatibility, and boundary cases, report exact reproducible evidence, and do not hide skipped or inconclusive checks.',
29
+ review: 'Review the implementation and evidence independently against the WorkItem contract and repository rules. Prioritize correctness, security, data loss, compatibility, and missing tests over style preferences. Return approved or changes_requested with concrete, actionable findings and evidence.',
30
+ document: 'Update the user or maintainer documentation required by the Action objective. Keep terminology and examples consistent with actual behavior, cover compatibility or operational consequences, and verify links, commands, and bilingual requirements where applicable.',
31
+ operate: 'Perform the operational change with explicit preconditions, safety fences, observability, and rollback handling. Verify the live or simulated postcondition from authoritative state, avoid destructive shortcuts, and record the exact evidence needed for handoff.',
32
+ deliver: 'Deliver only an approved result using the repository release policy. Recheck the reviewed commit and remote state, run required final verification on the immutable delivery tree, publish only the requested artifacts, and report commit, tag, deployment, and residual-risk evidence.',
33
+ write: 'Produce the requested written deliverable for its intended audience. Use the available evidence, preserve required terminology and structure, avoid unsupported claims, and verify the result against every acceptance criterion.',
34
+ custom: 'Complete the Action objective using repository facts and the WorkItem contract. State the approach, handle relevant risks and boundary conditions, produce the requested artifact or change, verify the result, and return concrete evidence plus any residual uncertainty.',
15
35
  });
16
36
 
17
37
  const DEFAULT_SOFTWARE_CHANGE_STAGES = Object.freeze([
@@ -94,6 +114,14 @@ export function defaultWorkCenterStageInstructions() {
94
114
  return { ...DEFAULT_STAGE_INSTRUCTIONS };
95
115
  }
96
116
 
117
+ function normalizeGlobalInstructions(value) {
118
+ return typeof value === 'string' ? value.trim().slice(0, 20_000) : '';
119
+ }
120
+
121
+ function generatedActionType(value) {
122
+ return cleanId(value, 'custom').slice(0, 64);
123
+ }
124
+
97
125
  export function normalizeWorkflowDefinition(value, index = 0) {
98
126
  if (!value || typeof value !== 'object' || Array.isArray(value)) {
99
127
  throw new Error('Work Center workflow must be an object');
@@ -103,10 +131,13 @@ export function normalizeWorkflowDefinition(value, index = 0) {
103
131
  if (!Array.isArray(value.stages) || value.stages.length === 0) {
104
132
  throw new Error(`Work Center workflow "${id}" requires at least one stage`);
105
133
  }
134
+ const planningMode = value.planningMode === 'ai' ? 'ai' : 'static';
106
135
  const seen = new Set();
107
136
  const stages = value.stages.map((rawStage, stageIndex) => {
108
137
  const source = rawStage && typeof rawStage === 'object' ? rawStage : {};
109
- const type = STAGE_TYPES.has(source.type) ? source.type : 'custom';
138
+ const type = planningMode === 'ai'
139
+ ? generatedActionType(source.type)
140
+ : (STAGE_TYPES.has(source.type) ? source.type : 'custom');
110
141
  const stageId = cleanId(source.id, `${type}-${stageIndex + 1}`);
111
142
  if (seen.has(stageId)) throw new Error(`Duplicate Work Center stage id: ${stageId}`);
112
143
  seen.add(stageId);
@@ -137,7 +168,6 @@ export function normalizeWorkflowDefinition(value, index = 0) {
137
168
  throw new Error(`Review stage "${stage.id}" must return to an earlier editable stage`);
138
169
  }
139
170
  }
140
- const planningMode = value.planningMode === 'ai' ? 'ai' : 'static';
141
171
  return {
142
172
  version: 1,
143
173
  id,
@@ -146,6 +176,7 @@ export function normalizeWorkflowDefinition(value, index = 0) {
146
176
  workItemType: typeof value.workItemType === 'string' && value.workItemType.trim()
147
177
  ? cleanId(value.workItemType, 'general')
148
178
  : null,
179
+ globalInstructions: normalizeGlobalInstructions(value.globalInstructions),
149
180
  modelPolicy: normalizeModelPolicy(value.modelPolicy),
150
181
  actionInstructions: normalizeActionInstructions(value.actionInstructions),
151
182
  stages,
@@ -169,6 +200,7 @@ export function defaultWorkCenterSettings() {
169
200
  defaultWorkflowId: 'software-change',
170
201
  startImmediately: true,
171
202
  defaultWorkDir: '',
203
+ globalInstructions: '',
172
204
  modelPolicy: { mode: 'inherit', model: null, effort: null },
173
205
  actionInstructions: normalizeActionInstructions(),
174
206
  workflows: [normalizeWorkflowDefinition({
@@ -204,6 +236,7 @@ export function normalizeWorkCenterSettings(value) {
204
236
  defaultWorkflowId,
205
237
  startImmediately: source.startImmediately !== false,
206
238
  defaultWorkDir: typeof source.defaultWorkDir === 'string' ? source.defaultWorkDir.trim() : '',
239
+ globalInstructions: normalizeGlobalInstructions(source.globalInstructions),
207
240
  modelPolicy: normalizeModelPolicy(source.modelPolicy || migratedModelPolicy),
208
241
  actionInstructions: normalizeActionInstructions(source.actionInstructions || migratedInstructions),
209
242
  workflows,
@@ -212,11 +245,12 @@ export function normalizeWorkCenterSettings(value) {
212
245
 
213
246
  export function resolvePlanningWorkflowSnapshot(settings) {
214
247
  const normalized = normalizeWorkCenterSettings(settings);
215
- const triageInstruction = `${normalized.actionInstructions.triage}\n\nDecide the smallest reliable execution flow for this specific WorkItem. Classify the WorkItem and return a structured plan. Do not copy a generic workflow when fewer Actions are sufficient.`;
248
+ const triageInstruction = `${normalized.actionInstructions.triage}\n\nChoose a specific workItemType and the smallest reliable sequence of 1 to 8 Actions for this WorkItem. Action types are extensible lowercase slugs: use a built-in type when its reusable policy fits, or define a precise domain type when it does not. For every Action, provide an objective that is independently executable and verifiable, plus the capability needed to select the right VP. Add research, design, diagnosis, migration, documentation, operations, testing, independent review, or delivery only when the task requires them. Do not copy a generic workflow.`;
216
249
  return normalizeWorkflowDefinition({
217
250
  id: 'ai-planned',
218
251
  name: 'AI planned',
219
252
  planningMode: 'ai',
253
+ globalInstructions: normalized.globalInstructions,
220
254
  modelPolicy: normalized.modelPolicy,
221
255
  actionInstructions: normalized.actionInstructions,
222
256
  stages: [{
@@ -266,24 +300,32 @@ export function applyGeneratedPlan(workItem, rawPlan) {
266
300
  if (!rawPlan || typeof rawPlan !== 'object' || Array.isArray(rawPlan)) {
267
301
  throw new Error('AI-planned triage requires a structured plan');
268
302
  }
269
- const workItemType = cleanId(rawPlan.workItemType, 'general');
303
+ if (typeof rawPlan.workItemType !== 'string' || !rawPlan.workItemType.trim()) {
304
+ throw new Error('AI-planned triage requires a specific workItemType');
305
+ }
306
+ const workItemType = cleanId(rawPlan.workItemType, '').slice(0, 64);
307
+ if (!workItemType) throw new Error('AI-planned triage requires a valid workItemType');
270
308
  if (!Array.isArray(rawPlan.actions) || rawPlan.actions.length < 1 || rawPlan.actions.length > 8) {
271
309
  throw new Error('AI-planned triage requires between 1 and 8 Actions');
272
310
  }
273
311
  const seen = new Set(['triage']);
274
312
  const generated = rawPlan.actions.map((rawAction, index) => {
275
313
  const input = rawAction && typeof rawAction === 'object' && !Array.isArray(rawAction) ? rawAction : {};
276
- const type = STAGE_TYPES.has(input.type) && input.type !== 'triage' ? input.type : 'custom';
314
+ const type = generatedActionType(input.type);
315
+ if (type === 'triage') throw new Error('AI-planned Actions cannot add another triage Action');
277
316
  const id = cleanId(input.id, `${type}-${index + 1}`);
278
317
  if (seen.has(id)) throw new Error(`Duplicate AI-planned Action id: ${id}`);
279
318
  seen.add(id);
280
319
  const objective = typeof input.objective === 'string' ? input.objective.trim().slice(0, 2_000) : '';
281
320
  if (!objective) throw new Error(`AI-planned Action "${id}" requires an objective`);
321
+ const actionInstruction = Object.hasOwn(source.actionInstructions, type)
322
+ ? source.actionInstructions[type]
323
+ : source.actionInstructions.custom;
282
324
  const stage = {
283
325
  id,
284
326
  name: String(input.name || id).trim().slice(0, 120) || id,
285
327
  type,
286
- instruction: `${source.actionInstructions[type] || source.actionInstructions.custom}\n\nAction objective for this WorkItem:\n${objective}`,
328
+ instruction: `${actionInstruction}\n\nAction type: ${type}\nAction objective for this WorkItem:\n${objective}`,
287
329
  assignmentPolicy: {
288
330
  mode: 'auto',
289
331
  capability: cleanId(input.capability, type),