@brawnen/agent-harness-cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,490 @@
1
+ import {
2
+ getActiveTask,
3
+ initTaskState,
4
+ resolveActiveTaskId,
5
+ requireTaskState,
6
+ setActiveTaskId,
7
+ updateTaskState
8
+ } from "./state-store.js";
9
+ import { loadProjectConfig } from "./project-config.js";
10
+ import { normalizeOutputPolicy } from "./output-policy.js";
11
+ import { evaluateTaskWorkflowDecision, normalizeWorkflowPolicy } from "./workflow-policy.js";
12
+
13
+ const CONTINUE_KEYWORDS = [
14
+ "继续",
15
+ "继续推进",
16
+ "继续做",
17
+ "按刚才",
18
+ "基于刚才",
19
+ "补测试",
20
+ "补一下测试",
21
+ "修一下",
22
+ "修掉",
23
+ "continue",
24
+ "follow up",
25
+ "follow-up"
26
+ ];
27
+
28
+ const NEW_TASK_KEYWORDS = [
29
+ "新任务",
30
+ "另一个问题",
31
+ "另外一个问题",
32
+ "另外",
33
+ "还有个问题",
34
+ "顺便",
35
+ "再做",
36
+ "new task",
37
+ "another task",
38
+ "another issue"
39
+ ];
40
+
41
+ const EXPLORE_KEYWORDS = ["调研", "解释", "分析", "看一下", "看看", "explore", "investigate"];
42
+ const BUG_KEYWORDS = ["bug", "报错", "错误", "异常", "修复", "fix", "error"];
43
+ const REFACTOR_KEYWORDS = ["重构", "refactor", "整理", "收敛结构"];
44
+ const PROTOTYPE_KEYWORDS = ["原型", "poc", "prototype", "草稿", "快速验证"];
45
+ const HIGH_RISK_KEYWORDS = ["rm -rf", "删除", "drop database", "reset --hard", ".idea/", "生产"];
46
+ const MANUAL_CONFIRMATION_KEYWORDS = [
47
+ "确认继续",
48
+ "继续执行",
49
+ "可以继续",
50
+ "继续吧",
51
+ "我确认",
52
+ "确认,可以",
53
+ "允许继续",
54
+ "就按这个做",
55
+ "按这个做",
56
+ "继续",
57
+ "go ahead",
58
+ "proceed"
59
+ ];
60
+ const FORCE_OVERRIDE_KEYWORDS = [
61
+ "别问了直接做",
62
+ "直接做",
63
+ "跳过验证",
64
+ "跳过确认",
65
+ "跳过澄清",
66
+ "不用确认",
67
+ "无需确认",
68
+ "不要再问",
69
+ "先别验证",
70
+ "skip verification",
71
+ "skip verify",
72
+ "force override"
73
+ ];
74
+
75
+ export function buildTaskDraftFromInput(sourceInput, options = {}) {
76
+ const input = String(sourceInput ?? "").trim();
77
+ if (!input) {
78
+ throw new Error("缺少任务描述");
79
+ }
80
+
81
+ const scope = normalizeArray(options.scope);
82
+ const acceptance = normalizeArray(options.acceptance);
83
+ const openQuestions = [];
84
+ const riskSignals = [];
85
+
86
+ if (scope.length === 0) {
87
+ openQuestions.push("当前任务的作用范围仍未明确,需要先澄清允许修改的模块、目录或主题。");
88
+ }
89
+
90
+ if (acceptance.length === 0) {
91
+ openQuestions.push("当前任务的完成标准仍未明确,需要先澄清 acceptance。");
92
+ }
93
+
94
+ const riskLevel = inferRiskLevel(input, riskSignals);
95
+ const intent = options.intent ?? inferIntent(input);
96
+ const mode = options.mode ?? (intent === "explore" ? "explore" : "delivery");
97
+ const nextAction = openQuestions.length > 0 ? "clarify" : "plan";
98
+ const derivedState = openQuestions.length > 0 ? "needs_clarification" : "planned";
99
+
100
+ return {
101
+ schema_version: "0.3",
102
+ source_input: input,
103
+ intent,
104
+ goal: options.goal ?? input,
105
+ scope: scope.length > 0 ? scope : ["待澄清作用范围"],
106
+ acceptance: acceptance.length > 0 ? acceptance : ["待澄清完成标准"],
107
+ title: options.title ?? null,
108
+ constraints: normalizeArray(options.constraints),
109
+ assumptions: normalizeArray(options.assumptions),
110
+ open_questions: openQuestions,
111
+ risk_signals: riskSignals,
112
+ context_refs: normalizeArray(options.contextRefs),
113
+ next_action: nextAction,
114
+ mode,
115
+ derived: {
116
+ risk_level: riskLevel,
117
+ state: derivedState
118
+ }
119
+ };
120
+ }
121
+
122
+ export function createTaskFromInput(cwd, sourceInput, options = {}) {
123
+ if (options.suspendActive) {
124
+ suspendActiveTask(cwd, {
125
+ reason: options.suspendReason ?? "当前输入被识别为新任务,旧任务已挂起。",
126
+ clearActive: false
127
+ });
128
+ }
129
+
130
+ const taskDraft = buildTaskDraftFromInput(sourceInput, options);
131
+ const projectConfig = loadProjectConfig(cwd);
132
+ const workflowDecision = evaluateTaskWorkflowDecision({
133
+ task_id: options.taskId ?? null,
134
+ current_state: taskDraft?.derived?.state ?? "draft",
135
+ task_draft: taskDraft,
136
+ confirmed_contract: null,
137
+ override_history: []
138
+ }, {
139
+ workflowPolicy: normalizeWorkflowPolicy(projectConfig?.workflow_policy),
140
+ outputPolicy: normalizeOutputPolicy(projectConfig?.output_policy)
141
+ });
142
+ return initTaskState(cwd, {
143
+ taskDraft,
144
+ taskId: options.taskId,
145
+ workflowDecision
146
+ });
147
+ }
148
+
149
+ export function suspendActiveTask(cwd, options = {}) {
150
+ const activeTaskId = options.taskId ?? resolveActiveTaskId(cwd);
151
+ if (!activeTaskId) {
152
+ return {
153
+ changed: false,
154
+ cleared_active_task: false,
155
+ reason: "当前无活跃任务",
156
+ task_id: null
157
+ };
158
+ }
159
+
160
+ const taskState = requireTaskState(cwd, activeTaskId);
161
+ const clearActive = options.clearActive ?? true;
162
+
163
+ if (taskState.current_state === "suspended") {
164
+ if (clearActive) {
165
+ setActiveTaskId(cwd, null);
166
+ }
167
+ return {
168
+ changed: false,
169
+ cleared_active_task: clearActive,
170
+ reason: "活跃任务已处于 suspended",
171
+ task_id: activeTaskId,
172
+ task_state: taskState
173
+ };
174
+ }
175
+
176
+ if (taskState.current_state === "done" || taskState.current_state === "failed") {
177
+ if (clearActive) {
178
+ setActiveTaskId(cwd, null);
179
+ }
180
+ return {
181
+ changed: false,
182
+ cleared_active_task: clearActive,
183
+ reason: `当前任务已是 ${taskState.current_state},无需挂起`,
184
+ task_id: activeTaskId,
185
+ task_state: taskState
186
+ };
187
+ }
188
+
189
+ const evidence = options.reason
190
+ ? [{
191
+ type: "reasoning_note",
192
+ content: options.reason,
193
+ timestamp: new Date().toISOString()
194
+ }]
195
+ : [];
196
+
197
+ const updatedTask = updateTaskState(cwd, activeTaskId, {
198
+ current_state: "suspended",
199
+ evidence
200
+ });
201
+
202
+ if (clearActive) {
203
+ setActiveTaskId(cwd, null);
204
+ }
205
+
206
+ return {
207
+ changed: true,
208
+ cleared_active_task: clearActive,
209
+ task_id: activeTaskId,
210
+ task_state: updatedTask
211
+ };
212
+ }
213
+
214
+ export function autoIntakePrompt(cwd, prompt) {
215
+ const input = String(prompt ?? "").trim();
216
+ if (!input) {
217
+ return {
218
+ action: "noop",
219
+ additionalContext: "",
220
+ decision: buildDecision("noop", {
221
+ reasonCode: "empty_prompt",
222
+ reason: "当前输入为空,不触发自动 intake。",
223
+ confidence: "high"
224
+ })
225
+ };
226
+ }
227
+
228
+ const activeTask = getActiveTask(cwd);
229
+ if (!activeTask) {
230
+ const createdTask = createTaskFromInput(cwd, input, { suspendActive: false });
231
+ return {
232
+ action: "created",
233
+ task: createdTask,
234
+ additionalContext: buildNewTaskContext(createdTask),
235
+ decision: buildDecision("new", {
236
+ reasonCode: "no_active_task",
237
+ reason: "当前没有 active task,自动创建新任务。",
238
+ confidence: "high"
239
+ })
240
+ };
241
+ }
242
+
243
+ const decision = classifyPromptAgainstTask(input, activeTask);
244
+ if (decision.type === "continue") {
245
+ return {
246
+ action: "continue",
247
+ task: activeTask,
248
+ additionalContext: buildCurrentTaskContext(activeTask),
249
+ decision
250
+ };
251
+ }
252
+
253
+ if (decision.type === "clarify") {
254
+ return {
255
+ action: "clarify",
256
+ task: activeTask,
257
+ block: decision.block,
258
+ reason: decision.reason,
259
+ additionalContext: decision.reason,
260
+ decision
261
+ };
262
+ }
263
+
264
+ suspendActiveTask(cwd, {
265
+ reason: "Codex UserPromptSubmit 将当前输入判定为新任务,旧任务已自动挂起。",
266
+ clearActive: false
267
+ });
268
+
269
+ const createdTask = createTaskFromInput(cwd, input, { suspendActive: false });
270
+ return {
271
+ action: "created",
272
+ task: createdTask,
273
+ additionalContext: buildNewTaskContext(createdTask),
274
+ decision
275
+ };
276
+ }
277
+
278
+ export function buildCurrentTaskContext(taskState) {
279
+ if (!taskState) {
280
+ return "";
281
+ }
282
+
283
+ const goal = taskState?.confirmed_contract?.goal ?? taskState?.task_draft?.goal ?? "未定义目标";
284
+ const nextAction = deriveNextAction(taskState);
285
+ const currentState = taskState?.current_state ?? "unknown";
286
+ const blockingQuestion = Array.isArray(taskState?.open_questions) && taskState.open_questions.length > 0
287
+ ? ` 当前阻断问题:${taskState.open_questions[0]}`
288
+ : "";
289
+
290
+ return `当前 active task: ${taskState.task_id}。目标:${goal}。当前状态:${currentState}。下一步动作:${nextAction}。${blockingQuestion}`.trim();
291
+ }
292
+
293
+ export function buildNewTaskContext(taskState) {
294
+ if (!taskState) {
295
+ return "";
296
+ }
297
+
298
+ const draft = taskState.task_draft ?? {};
299
+ return `已自动创建新任务 ${taskState.task_id}。intent=${draft.intent},goal=${draft.goal},next_action=${draft.next_action}。请先按该任务继续。`;
300
+ }
301
+
302
+ export function classifyUserOverridePrompt(prompt) {
303
+ const normalizedPrompt = String(prompt ?? "").trim().toLowerCase();
304
+ if (!normalizedPrompt) {
305
+ return null;
306
+ }
307
+
308
+ const matchedForceOverrideKeyword = findMatchedKeyword(normalizedPrompt, FORCE_OVERRIDE_KEYWORDS);
309
+ if (matchedForceOverrideKeyword) {
310
+ return {
311
+ type: "force_override",
312
+ reason_code: "matched_force_override_keyword",
313
+ reason: `输入命中 force override 关键词:${matchedForceOverrideKeyword}。`,
314
+ matched_signals: [`keyword:${matchedForceOverrideKeyword}`],
315
+ confidence: "high"
316
+ };
317
+ }
318
+
319
+ const matchedManualConfirmationKeyword = findMatchedKeyword(normalizedPrompt, MANUAL_CONFIRMATION_KEYWORDS);
320
+ if (matchedManualConfirmationKeyword) {
321
+ return {
322
+ type: "manual_confirmation",
323
+ reason_code: "matched_manual_confirmation_keyword",
324
+ reason: `输入命中人工确认关键词:${matchedManualConfirmationKeyword}。`,
325
+ matched_signals: [`keyword:${matchedManualConfirmationKeyword}`],
326
+ confidence: "medium"
327
+ };
328
+ }
329
+
330
+ return null;
331
+ }
332
+
333
+ function classifyPromptAgainstTask(prompt, activeTask) {
334
+ const normalizedPrompt = prompt.toLowerCase();
335
+ const activeGoal = String(activeTask?.confirmed_contract?.goal ?? activeTask?.task_draft?.goal ?? "").toLowerCase();
336
+ const activeScope = normalizeArray(activeTask?.confirmed_contract?.scope ?? activeTask?.task_draft?.scope).map((item) => item.toLowerCase());
337
+ const currentState = activeTask?.current_state ?? "unknown";
338
+
339
+ if (["done", "failed", "suspended"].includes(currentState)) {
340
+ return buildDecision("new", {
341
+ reasonCode: "inactive_active_task_state",
342
+ reason: `当前 active task 状态为 ${currentState},自动按新任务处理。`,
343
+ matchedSignals: [`active_state:${currentState}`],
344
+ confidence: "high"
345
+ });
346
+ }
347
+
348
+ const matchedNewKeyword = findMatchedKeyword(normalizedPrompt, NEW_TASK_KEYWORDS);
349
+ if (matchedNewKeyword) {
350
+ return buildDecision("new", {
351
+ reasonCode: "matched_new_task_keyword",
352
+ reason: `输入命中新任务关键词:${matchedNewKeyword}。`,
353
+ matchedSignals: [`keyword:${matchedNewKeyword}`],
354
+ confidence: "high"
355
+ });
356
+ }
357
+
358
+ const matchedContinueKeyword = findMatchedKeyword(normalizedPrompt, CONTINUE_KEYWORDS);
359
+ if (matchedContinueKeyword) {
360
+ return buildDecision("continue", {
361
+ reasonCode: "matched_continue_keyword",
362
+ reason: `输入命中续写关键词:${matchedContinueKeyword}。`,
363
+ matchedSignals: [`keyword:${matchedContinueKeyword}`],
364
+ confidence: "high"
365
+ });
366
+ }
367
+
368
+ const activeGoalFragment = activeGoal.slice(0, Math.min(activeGoal.length, 12)).trim();
369
+ if (activeGoalFragment && normalizedPrompt.includes(activeGoalFragment)) {
370
+ return buildDecision("continue", {
371
+ reasonCode: "matched_active_goal_fragment",
372
+ reason: `输入命中当前任务目标片段:${activeGoalFragment}。`,
373
+ matchedSignals: [`goal_fragment:${activeGoalFragment}`],
374
+ confidence: "medium"
375
+ });
376
+ }
377
+
378
+ const matchedScope = activeScope.find((scopeItem) => (
379
+ scopeItem &&
380
+ scopeItem !== "待澄清作用范围" &&
381
+ normalizedPrompt.includes(scopeItem)
382
+ ));
383
+ if (matchedScope) {
384
+ return buildDecision("continue", {
385
+ reasonCode: "matched_active_scope",
386
+ reason: `输入命中当前任务 scope:${matchedScope}。`,
387
+ matchedSignals: [`scope:${matchedScope}`],
388
+ confidence: "medium"
389
+ });
390
+ }
391
+
392
+ const ambiguous = isAmbiguousPrompt(prompt);
393
+ if (ambiguous) {
394
+ const matchedHighRiskKeyword = findMatchedKeyword(normalizedPrompt, HIGH_RISK_KEYWORDS);
395
+ const highRisk = Boolean(matchedHighRiskKeyword);
396
+ return buildDecision("clarify", {
397
+ block: highRisk,
398
+ reasonCode: highRisk ? "ambiguous_high_risk_prompt" : "ambiguous_prompt",
399
+ reason: highRisk
400
+ ? "当前输入任务归属不明且包含高风险信号。先澄清是在延续旧任务还是新任务,再继续执行。"
401
+ : "当前输入无法可靠判断是在延续旧任务还是新任务。先向用户澄清任务归属,再继续执行。",
402
+ matchedSignals: highRisk
403
+ ? [`high_risk_keyword:${matchedHighRiskKeyword}`, "ambiguous_prompt"]
404
+ : ["ambiguous_prompt"],
405
+ confidence: highRisk ? "high" : "low"
406
+ });
407
+ }
408
+
409
+ return buildDecision("new", {
410
+ reasonCode: "fallback_new_task",
411
+ reason: "未命中续写信号,且输入不属于歧义短句,按新任务处理。",
412
+ matchedSignals: ["fallback_new_task"],
413
+ confidence: "low"
414
+ });
415
+ }
416
+
417
+ function isAmbiguousPrompt(prompt) {
418
+ const normalized = String(prompt ?? "").trim();
419
+ if (normalized.length <= 8) {
420
+ return true;
421
+ }
422
+
423
+ const tokens = normalized.split(/\s+/).filter(Boolean);
424
+ if (tokens.length <= 2 && normalized.length <= 18) {
425
+ return true;
426
+ }
427
+
428
+ return ["看一下这个", "有个问题", "帮我处理一下", "处理下", "看看这个"].some((text) => normalized.includes(text));
429
+ }
430
+
431
+ function inferIntent(input) {
432
+ const normalized = input.toLowerCase();
433
+ if (EXPLORE_KEYWORDS.some((keyword) => normalized.includes(keyword))) {
434
+ return "explore";
435
+ }
436
+ if (BUG_KEYWORDS.some((keyword) => normalized.includes(keyword))) {
437
+ return "bug";
438
+ }
439
+ if (REFACTOR_KEYWORDS.some((keyword) => normalized.includes(keyword))) {
440
+ return "refactor";
441
+ }
442
+ if (PROTOTYPE_KEYWORDS.some((keyword) => normalized.includes(keyword))) {
443
+ return "prototype";
444
+ }
445
+ return "feature";
446
+ }
447
+
448
+ function inferRiskLevel(input, riskSignals) {
449
+ const normalized = input.toLowerCase();
450
+ for (const keyword of HIGH_RISK_KEYWORDS) {
451
+ if (normalized.includes(keyword)) {
452
+ riskSignals.push(`命中高风险关键词: ${keyword}`);
453
+ return "high";
454
+ }
455
+ }
456
+ return "low";
457
+ }
458
+
459
+ function normalizeArray(value) {
460
+ if (Array.isArray(value)) {
461
+ return value.filter(Boolean).map((item) => String(item).trim()).filter(Boolean);
462
+ }
463
+ if (value == null) {
464
+ return [];
465
+ }
466
+ return [String(value).trim()].filter(Boolean);
467
+ }
468
+
469
+ function buildDecision(type, options = {}) {
470
+ return {
471
+ type,
472
+ block: options.block ?? false,
473
+ reason_code: options.reasonCode ?? "unspecified",
474
+ reason: options.reason ?? "",
475
+ matched_signals: Array.isArray(options.matchedSignals) ? options.matchedSignals : [],
476
+ confidence: options.confidence ?? "low"
477
+ };
478
+ }
479
+
480
+ function findMatchedKeyword(input, keywords) {
481
+ return keywords.find((keyword) => input.includes(keyword)) ?? null;
482
+ }
483
+
484
+ function deriveNextAction(taskState) {
485
+ const phase = taskState?.current_phase ?? null;
486
+ if (phase && ["clarify", "plan", "execute", "verify", "report", "close"].includes(phase)) {
487
+ return phase;
488
+ }
489
+ return taskState?.task_draft?.next_action ?? "plan";
490
+ }