@capstone-nomos/cli 0.1.13 → 0.1.15
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.
|
@@ -3,29 +3,48 @@ import path from 'node:path';
|
|
|
3
3
|
// 브리핑으로 프롬프트에 넣은 인계 노트의 id. 제출할 때 "이미 받은 노트"로 서버에 함께 보낸다.
|
|
4
4
|
// 서버는 관련 노트를 전부 확인해야 제출을 받는다(NOTES_UNACKNOWLEDGED). 프롬프트로 전달된 노트는 확인한 것이므로
|
|
5
5
|
// 모델이 id를 옮겨 적게 하지 않고 브릿지가 넣는다 — 모델이 직접 넣는 건 반려 뒤 새로 받은 노트뿐이다.
|
|
6
|
+
// 질문으로 정해진 결정(decisions)도 같다 — 브리핑으로 받은 결정 id를 함께 남기고 제출 때 보낸다(DECISIONS_UNACKNOWLEDGED).
|
|
6
7
|
// Executor가 작업공간에 쓰고(브리핑 직후), MCP 서버의 submit_artifact가 읽는다. 둘은 다른 프로세스다.
|
|
7
8
|
export const BRIEFING_NOTES_FILE = '.nomos-briefing.json';
|
|
8
|
-
export function writeBriefingNotes(workspaceDir, taskId, noteIds) {
|
|
9
|
-
writeFileSync(path.join(workspaceDir, BRIEFING_NOTES_FILE), `${JSON.stringify({ taskId, noteIds }, null, 2)}\n`);
|
|
9
|
+
export function writeBriefingNotes(workspaceDir, taskId, noteIds, decisionIds = []) {
|
|
10
|
+
writeFileSync(path.join(workspaceDir, BRIEFING_NOTES_FILE), `${JSON.stringify({ taskId, noteIds, decisionIds }, null, 2)}\n`);
|
|
10
11
|
}
|
|
11
|
-
// 다른 태스크의 파일이거나 없으면 빈 목록 — 엉뚱한 태스크의
|
|
12
|
-
|
|
12
|
+
// 다른 태스크의 파일이거나 없으면 빈 목록 — 엉뚱한 태스크의 노트·결정을 확인한 것으로 보내지 않는다.
|
|
13
|
+
function readBriefingIds(workspaceDir, taskId, key) {
|
|
13
14
|
const file = path.join(workspaceDir, BRIEFING_NOTES_FILE);
|
|
14
15
|
if (!existsSync(file))
|
|
15
16
|
return [];
|
|
16
17
|
try {
|
|
17
18
|
const parsed = JSON.parse(readFileSync(file, 'utf-8'));
|
|
18
|
-
|
|
19
|
+
const ids = parsed[key];
|
|
20
|
+
if (parsed.taskId !== taskId || !Array.isArray(ids))
|
|
19
21
|
return [];
|
|
20
|
-
return
|
|
22
|
+
return ids.filter((id) => typeof id === 'string');
|
|
21
23
|
}
|
|
22
24
|
catch {
|
|
23
25
|
return [];
|
|
24
26
|
}
|
|
25
27
|
}
|
|
28
|
+
export function readBriefingNoteIds(workspaceDir, taskId) {
|
|
29
|
+
return readBriefingIds(workspaceDir, taskId, 'noteIds');
|
|
30
|
+
}
|
|
31
|
+
export function readBriefingDecisionIds(workspaceDir, taskId) {
|
|
32
|
+
return readBriefingIds(workspaceDir, taskId, 'decisionIds');
|
|
33
|
+
}
|
|
26
34
|
export function mergeAcknowledged(briefed, fromModel) {
|
|
27
35
|
return [...new Set([...briefed, ...(fromModel ?? [])])].sort();
|
|
28
36
|
}
|
|
37
|
+
// submit_artifact가 서버에 보내는 본문. 브리핑으로 받은 노트·결정과 모델이 반려 뒤 확인한 것을 합친다.
|
|
38
|
+
// acknowledgedDecisionIds는 비어 있어도 **항상** 보낸다 — 서버는 이 필드가 없으면 옛 CLI(0.1.14 이하)로 보고 결정 확인을 건너뛴다.
|
|
39
|
+
export function buildSubmitBody(input) {
|
|
40
|
+
const acknowledgedNoteIds = mergeAcknowledged(readBriefingNoteIds(input.workspaceDir, input.taskId), input.acknowledgedNoteIds);
|
|
41
|
+
return {
|
|
42
|
+
commitSha: input.commitSha,
|
|
43
|
+
changedPaths: input.changedPaths,
|
|
44
|
+
...(acknowledgedNoteIds.length === 0 ? {} : { acknowledgedNoteIds }),
|
|
45
|
+
acknowledgedDecisionIds: mergeAcknowledged(readBriefingDecisionIds(input.workspaceDir, input.taskId), input.acknowledgedDecisionIds),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
29
48
|
// 지금 작업공간의 태스크 id — 권한 도구가 AskUserQuestion을 어느 태스크의 질문으로 올릴지 정한다. 없으면 null.
|
|
30
49
|
export function readBriefingTaskId(workspaceDir) {
|
|
31
50
|
const file = path.join(workspaceDir, BRIEFING_NOTES_FILE);
|
|
@@ -7,7 +7,7 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
|
7
7
|
import { z } from 'zod';
|
|
8
8
|
import { readCredentials, updateAccessToken } from './credentials.js';
|
|
9
9
|
import { NomosClient, PolicyStaleLoopError } from './nomos-client.js';
|
|
10
|
-
import {
|
|
10
|
+
import { buildSubmitBody, readBriefingTaskId } from './briefing-notes.js';
|
|
11
11
|
import { PERMISSION_TOOL } from './claude-args.js';
|
|
12
12
|
import { DENY_OTHER, denyUnanswered, handleAskUserQuestion, isHandled } from './permission.js';
|
|
13
13
|
import { pushTaskBranch } from './push.js';
|
|
@@ -72,7 +72,7 @@ server.registerTool('claim_task', {
|
|
|
72
72
|
const workspaceDir = process.env.NOMOS_WORKSPACE_DIR ?? process.cwd();
|
|
73
73
|
server.registerTool('submit_artifact', {
|
|
74
74
|
title: 'Submit an artifact',
|
|
75
|
-
description: 'Submit finished work for a task you have claimed. Commit your work on the task branch first, then provide the latest commit sha and every path changed on the task branch (including commits from earlier attempts on this branch). This tool pushes the task branch before submitting; if the push is refused (wrong branch, commit not on the task branch, remote has diverged) nothing is submitted and you get the reason. The server re-checks the paths against the ownership rules, so listing paths you did not change (or omitting ones you did) will be rejected or will fail verification later. If handover notes related to your task were published after you started, the submission is rejected with NOTES_UNACKNOWLEDGED and the notes are returned: read them, change and commit your work if they affect it, then submit again with their ids in acknowledged_note_ids. Notes that were already in your instructions are acknowledged automatically.',
|
|
75
|
+
description: 'Submit finished work for a task you have claimed. Commit your work on the task branch first, then provide the latest commit sha and every path changed on the task branch (including commits from earlier attempts on this branch). This tool pushes the task branch before submitting; if the push is refused (wrong branch, commit not on the task branch, remote has diverged) nothing is submitted and you get the reason. The server re-checks the paths against the ownership rules, so listing paths you did not change (or omitting ones you did) will be rejected or will fail verification later. If handover notes related to your task were published after you started, the submission is rejected with NOTES_UNACKNOWLEDGED and the notes are returned: read them, change and commit your work if they affect it, then submit again with their ids in acknowledged_note_ids. Notes that were already in your instructions are acknowledged automatically. Likewise, if decisions were made by answering other tasks\' questions after you started, the submission is rejected with DECISIONS_UNACKNOWLEDGED and the decisions are returned (with any new related notes): follow them, change and commit your work if it differs, then submit again with their ids in acknowledged_decision_ids (and the notes\' ids in acknowledged_note_ids). Decisions already in your instructions are acknowledged automatically.',
|
|
76
76
|
inputSchema: {
|
|
77
77
|
task_id: z.string().describe('The claimed task id'),
|
|
78
78
|
commit_sha: z.string().describe('Commit sha containing the work'),
|
|
@@ -81,8 +81,12 @@ server.registerTool('submit_artifact', {
|
|
|
81
81
|
.array(z.string())
|
|
82
82
|
.optional()
|
|
83
83
|
.describe('Ids of handover notes you read after a NOTES_UNACKNOWLEDGED rejection'),
|
|
84
|
+
acknowledged_decision_ids: z
|
|
85
|
+
.array(z.string())
|
|
86
|
+
.optional()
|
|
87
|
+
.describe('Ids of decisions you read after a DECISIONS_UNACKNOWLEDGED rejection'),
|
|
84
88
|
},
|
|
85
|
-
}, async ({ task_id, commit_sha, changed_paths, acknowledged_note_ids }) => run(async () => {
|
|
89
|
+
}, async ({ task_id, commit_sha, changed_paths, acknowledged_note_ids, acknowledged_decision_ids }) => run(async () => {
|
|
86
90
|
// push할 브랜치와 막을 브랜치는 서버가 정한다. 모델이 넘긴 값으로 고르지 않는다.
|
|
87
91
|
const briefing = (await client.getBriefing(task_id));
|
|
88
92
|
const push = await pushTaskBranch({
|
|
@@ -95,13 +99,15 @@ server.registerTool('submit_artifact', {
|
|
|
95
99
|
process.stderr.write('[nomos-mcp] origin 없음 — push 건너뜀(서버가 mirror 모드로 로컬에서 읽는다)\n');
|
|
96
100
|
}
|
|
97
101
|
// push가 실패하면(PushRefused) 여기까지 오지 않는다 — 제출하지 않고 run()이 모델에게 사유를 돌려준다.
|
|
98
|
-
// 브리핑으로 프롬프트에 들어간
|
|
99
|
-
const
|
|
100
|
-
|
|
102
|
+
// 브리핑으로 프롬프트에 들어간 노트·결정은 이미 받은 것이다 — Executor가 작업공간에 남긴 id를 함께 보낸다.
|
|
103
|
+
const artifact = await client.submitArtifact(task_id, buildSubmitBody({
|
|
104
|
+
workspaceDir,
|
|
105
|
+
taskId: task_id,
|
|
101
106
|
commitSha: commit_sha,
|
|
102
107
|
changedPaths: changed_paths,
|
|
103
|
-
...(
|
|
104
|
-
|
|
108
|
+
...(acknowledged_note_ids === undefined ? {} : { acknowledgedNoteIds: acknowledged_note_ids }),
|
|
109
|
+
...(acknowledged_decision_ids === undefined ? {} : { acknowledgedDecisionIds: acknowledged_decision_ids }),
|
|
110
|
+
}));
|
|
105
111
|
return { ...artifact, push };
|
|
106
112
|
}, 'submit_artifact'));
|
|
107
113
|
server.registerTool('publish_note', {
|
|
@@ -44,7 +44,9 @@ export class NomosClient {
|
|
|
44
44
|
async claimTask(taskId) {
|
|
45
45
|
return (await this.request('POST', `/api/tasks/${taskId}/claim`));
|
|
46
46
|
}
|
|
47
|
-
async submitArtifact(taskId,
|
|
47
|
+
async submitArtifact(taskId,
|
|
48
|
+
// acknowledgedDecisionIds: MCP submit_artifact는 비어 있어도 항상 보낸다(없으면 서버가 옛 CLI로 보고 결정 확인을 건너뛴다).
|
|
49
|
+
input) {
|
|
48
50
|
return (await this.request('POST', `/api/tasks/${taskId}/artifacts`, input));
|
|
49
51
|
}
|
|
50
52
|
// 브릿지 단계(V2·V4) 보고. 서버가 판정하는 V1A·V1B·V3는 여기로 보낼 수 없다 — 403이 돌아온다.
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { closeSync, existsSync, mkdirSync, openSync, readFileSync, readSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { nomosHome } from '../bridge/credentials.js';
|
|
5
|
+
export function daemonFile() {
|
|
6
|
+
return path.join(nomosHome(), 'daemon.json');
|
|
7
|
+
}
|
|
8
|
+
export function logDir() {
|
|
9
|
+
return path.join(nomosHome(), 'logs');
|
|
10
|
+
}
|
|
11
|
+
export function readDaemon() {
|
|
12
|
+
try {
|
|
13
|
+
return JSON.parse(readFileSync(daemonFile(), 'utf-8'));
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
// 그 pid의 프로세스가 살아 있는가(신호 0은 확인만 한다).
|
|
20
|
+
export function isAlive(pid) {
|
|
21
|
+
try {
|
|
22
|
+
process.kill(pid, 0);
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
catch (err) {
|
|
26
|
+
return err.code === 'EPERM';
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
// 살아 있는 백그라운드 connect. 죽었는데 파일만 남았으면 지우고 null.
|
|
30
|
+
export function runningDaemon() {
|
|
31
|
+
const info = readDaemon();
|
|
32
|
+
if (!info)
|
|
33
|
+
return null;
|
|
34
|
+
if (isAlive(info.pid))
|
|
35
|
+
return info;
|
|
36
|
+
rmSync(daemonFile(), { force: true });
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
// --background를 뺀 같은 인자로 connect를 분리된 자식으로 띄운다. 이 프로세스는 바로 끝나도 자식은 계속 돈다.
|
|
40
|
+
export function startDaemon(input) {
|
|
41
|
+
const existing = runningDaemon();
|
|
42
|
+
if (existing)
|
|
43
|
+
throw new Error(`이미 백그라운드에서 돌고 있다(pid ${existing.pid}, 서버 ${existing.server}). 끄려면 nomos stop`);
|
|
44
|
+
mkdirSync(logDir(), { recursive: true });
|
|
45
|
+
const logFile = path.join(logDir(), 'connect.log');
|
|
46
|
+
const fd = openSync(logFile, 'a');
|
|
47
|
+
const args = input.args.filter((a) => a !== '--background');
|
|
48
|
+
const child = spawn(process.execPath, [input.cliPath, 'connect', ...args], {
|
|
49
|
+
detached: true,
|
|
50
|
+
stdio: ['ignore', fd, fd],
|
|
51
|
+
windowsHide: true,
|
|
52
|
+
env: { ...process.env, NOMOS_BACKGROUND: '1' },
|
|
53
|
+
});
|
|
54
|
+
closeSync(fd);
|
|
55
|
+
if (child.pid === undefined)
|
|
56
|
+
throw new Error('백그라운드 프로세스를 띄우지 못했다');
|
|
57
|
+
child.unref();
|
|
58
|
+
const info = { pid: child.pid, startedAt: new Date().toISOString(), server: input.server, logFile, args };
|
|
59
|
+
mkdirSync(nomosHome(), { recursive: true });
|
|
60
|
+
writeFileSync(daemonFile(), JSON.stringify(info, null, 2), { mode: 0o600 });
|
|
61
|
+
return info;
|
|
62
|
+
}
|
|
63
|
+
export function stopDaemon() {
|
|
64
|
+
const info = runningDaemon();
|
|
65
|
+
if (!info)
|
|
66
|
+
return null;
|
|
67
|
+
try {
|
|
68
|
+
process.kill(info.pid);
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
// 이미 끝났다.
|
|
72
|
+
}
|
|
73
|
+
rmSync(daemonFile(), { force: true });
|
|
74
|
+
return info;
|
|
75
|
+
}
|
|
76
|
+
// 로그 끝부분(최대 n줄). 파일이 크면 끝 64KB만 읽는다.
|
|
77
|
+
export function tailLog(file, lines = 15) {
|
|
78
|
+
if (!existsSync(file))
|
|
79
|
+
return [];
|
|
80
|
+
const size = statSync(file).size;
|
|
81
|
+
const start = Math.max(0, size - 64 * 1024);
|
|
82
|
+
const fd = openSync(file, 'r');
|
|
83
|
+
try {
|
|
84
|
+
const buf = Buffer.alloc(size - start);
|
|
85
|
+
readSync(fd, buf, 0, buf.length, start);
|
|
86
|
+
return buf.toString('utf-8').split(/\r?\n/).filter(Boolean).slice(-lines);
|
|
87
|
+
}
|
|
88
|
+
finally {
|
|
89
|
+
closeSync(fd);
|
|
90
|
+
}
|
|
91
|
+
}
|
package/dist/executor/cli.js
CHANGED
|
@@ -27,6 +27,7 @@ import { credentialsPath, readCredentials, updateAccessToken, writeCredentials,
|
|
|
27
27
|
import { NomosApiError, NomosClient } from '../bridge/nomos-client.js';
|
|
28
28
|
import { ActivityReporter, ActivityTracker, RunObserver } from './activity.js';
|
|
29
29
|
import { accountLine, connect, DEFAULT_SERVER, serverCalls } from './connect.js';
|
|
30
|
+
import { runningDaemon, startDaemon, stopDaemon, tailLog } from './background.js';
|
|
30
31
|
import { deviceLogin } from './device-login.js';
|
|
31
32
|
import { cliVersion, mcpServerPath } from './paths.js';
|
|
32
33
|
import { checkTools } from './preflight.js';
|
|
@@ -189,8 +190,8 @@ async function handleTask(client, projectId, task) {
|
|
|
189
190
|
});
|
|
190
191
|
log(`작업공간 ${workspace.dir} (브랜치 ${workspace.branch})`);
|
|
191
192
|
activeWorkspaces.set(task.id, { dir: workspace.dir, repo: briefing.repo.fullName });
|
|
192
|
-
// 프롬프트에 넣는
|
|
193
|
-
writeBriefingNotes(workspace.dir, task.id, (briefing.notes ?? []).map((n) => n.id));
|
|
193
|
+
// 프롬프트에 넣는 노트·결정 = 제출 때 "받은 것"으로 함께 보낼 노트·결정. MCP 서버(submit_artifact)가 읽는다.
|
|
194
|
+
writeBriefingNotes(workspace.dir, task.id, (briefing.notes ?? []).map((n) => n.id), (briefing.decisions ?? []).map((d) => d.id));
|
|
194
195
|
if (workspace.createdBranch) {
|
|
195
196
|
await client.reportBranch(task.id, workspace.branch);
|
|
196
197
|
}
|
|
@@ -400,7 +401,7 @@ async function postJson(url, body) {
|
|
|
400
401
|
return json.data;
|
|
401
402
|
}
|
|
402
403
|
function parseArgs(argv) {
|
|
403
|
-
const out = { positional: undefined, server: undefined, name: undefined, code: undefined, connectKey: false, json: false };
|
|
404
|
+
const out = { positional: undefined, server: undefined, name: undefined, code: undefined, connectKey: false, json: false, background: false };
|
|
404
405
|
for (let i = 0; i < argv.length; i += 1) {
|
|
405
406
|
const arg = argv[i];
|
|
406
407
|
if (arg === '--name')
|
|
@@ -411,6 +412,8 @@ function parseArgs(argv) {
|
|
|
411
412
|
out.connectKey = true;
|
|
412
413
|
else if (arg === '--code')
|
|
413
414
|
out.code = argv[(i += 1)];
|
|
415
|
+
else if (arg === '--background')
|
|
416
|
+
out.background = true;
|
|
414
417
|
else if (arg === '--json')
|
|
415
418
|
out.json = true;
|
|
416
419
|
else
|
|
@@ -490,6 +493,19 @@ async function connectCommand(argv) {
|
|
|
490
493
|
log(`${t.name}을 실행할 수 없다: ${t.detail}\n → ${t.hint}`);
|
|
491
494
|
throw new Error('필요한 도구가 없어 시작하지 않는다');
|
|
492
495
|
}
|
|
496
|
+
// 백그라운드(창 없이) — 같은 인자로 connect를 분리된 자식으로 띄우고 여기서 끝난다. 브라우저 승인 로그인은 화면이 필요하므로
|
|
497
|
+
// 이 서버의 자격 증명이 없으면 --code(1회용 연결 코드)와 함께만 받는다.
|
|
498
|
+
if (args.background) {
|
|
499
|
+
const saved = readCredentials();
|
|
500
|
+
if (!args.code && (!saved || saved.baseUrl !== server)) {
|
|
501
|
+
throw new Error('백그라운드로 띄우려면 먼저 한 번 connect로 로그인하거나, 웹의 연결 명령(--code)에 --background를 붙여라');
|
|
502
|
+
}
|
|
503
|
+
const info = startDaemon({ cliPath: process.argv[1], args: argv, server });
|
|
504
|
+
log(`백그라운드에서 실행한다 — pid ${info.pid}. 창을 닫아도 계속 돈다`);
|
|
505
|
+
log(`로그: ${info.logFile}`);
|
|
506
|
+
log('상태: nomos status · 끄기: nomos stop · 진행 상황은 웹의 룸에서 본다');
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
493
509
|
// 배정이 풀리면(해제·프로젝트 중단) 창을 닫지 않고 다시 배정을 기다린다. 연결 해제면 멈춘다.
|
|
494
510
|
// 1회용 코드는 처음 한 번만 쓴다 — 그 뒤로는 그 코드로 받은 자격 증명이 저장돼 있다.
|
|
495
511
|
let code = args.code?.trim() || undefined;
|
|
@@ -575,6 +591,17 @@ async function pmWorker() {
|
|
|
575
591
|
await new Promise((resolve) => setTimeout(resolve, PM_POLL_INTERVAL_MS));
|
|
576
592
|
}
|
|
577
593
|
}
|
|
594
|
+
function daemonStatus() {
|
|
595
|
+
const info = runningDaemon();
|
|
596
|
+
if (!info) {
|
|
597
|
+
log('백그라운드에서 돌고 있는 connect가 없다 (켜려면 connect --background)');
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
log(`실행 중 — pid ${info.pid}, ${info.startedAt}부터, 서버 ${info.server}`);
|
|
601
|
+
log(`로그: ${info.logFile}`);
|
|
602
|
+
for (const line of tailLog(info.logFile))
|
|
603
|
+
process.stdout.write(` ${line}\n`);
|
|
604
|
+
}
|
|
578
605
|
async function main() {
|
|
579
606
|
const command = process.argv[2];
|
|
580
607
|
if (command === 'connect')
|
|
@@ -587,6 +614,13 @@ async function main() {
|
|
|
587
614
|
}
|
|
588
615
|
if (command === 'login')
|
|
589
616
|
return login(process.argv.slice(3));
|
|
617
|
+
if (command === 'status')
|
|
618
|
+
return daemonStatus();
|
|
619
|
+
if (command === 'stop') {
|
|
620
|
+
const stopped = stopDaemon();
|
|
621
|
+
log(stopped ? `백그라운드 connect를 껐다(pid ${stopped.pid})` : '백그라운드에서 돌고 있는 connect가 없다');
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
590
624
|
if (command === 'refresh')
|
|
591
625
|
return refresh();
|
|
592
626
|
if (command === 'once')
|
|
@@ -604,7 +638,8 @@ async function main() {
|
|
|
604
638
|
}
|
|
605
639
|
process.stderr.write([
|
|
606
640
|
'사용법: nomos <명령>',
|
|
607
|
-
' connect [--code 코드] [--server 주소] [--name 이름] 로그인 → 배정 대기 → 작업 시작 (--code: 웹이 준 1회용 코드,
|
|
641
|
+
' connect [--code 코드] [--server 주소] [--name 이름] [--background] 로그인 → 배정 대기 → 작업 시작 (--code: 웹이 준 1회용 코드, --background: 창 없이)',
|
|
642
|
+
' status | stop 백그라운드 connect 상태 보기 · 끄기',
|
|
608
643
|
' doctor 설치 상태 점검',
|
|
609
644
|
' login [주소] [--name 이름] [--connect-key] 로그인만',
|
|
610
645
|
' refresh | once | start | clean | --version',
|
package/dist/executor/prompt.js
CHANGED
|
@@ -42,6 +42,24 @@ export function buildTaskPrompt(briefing, branch, environment) {
|
|
|
42
42
|
'이 브랜치에 이전 작업이 남아 있으면 이어서 진행한다. 같은 질문을 다시 묻지 않는다.',
|
|
43
43
|
].join('\n'));
|
|
44
44
|
}
|
|
45
|
+
// 다른 역할·PM이 질문에 답해 확정한 결정 — 이 태스크가 물은 것이 아니어도 프로젝트 전체가 따른다.
|
|
46
|
+
// 실행 중에는 끼워 넣지 않는다. 작업 중에 새로 정해진 결정은 제출 때 DECISIONS_UNACKNOWLEDGED로 온다.
|
|
47
|
+
if (briefing.decisions && briefing.decisions.length > 0) {
|
|
48
|
+
sections.push([
|
|
49
|
+
'# 질문으로 정해진 결정',
|
|
50
|
+
'다른 역할·PM이 질문에 답해 확정한 결정이다. 구현이 이 결정과 다르면 결정을 따른다.',
|
|
51
|
+
'결정 자체가 틀렸거나 바꿔야 한다고 보면 임의로 바꾸지 말고 AskUserQuestion으로 그 역할에 다시 묻는다.',
|
|
52
|
+
...briefing.decisions.map((d) => `- [물은 쪽 ${d.askerRole} · 정한 쪽 ${d.targetRole}${d.decidedByName ? ` ${d.decidedByName}` : ''}] Q: ${d.question}\n A: ${d.answer ?? ''}`),
|
|
53
|
+
].join('\n'));
|
|
54
|
+
}
|
|
55
|
+
// 사람(그 역할 담당·대표)이 룸에서 보낸 메시지 — 이번 실행이 시작되기 전에 온 것만 들어온다(실행 중에는 끼워 넣지 않는다).
|
|
56
|
+
if (briefing.humanMessages && briefing.humanMessages.length > 0) {
|
|
57
|
+
sections.push([
|
|
58
|
+
'# 담당자가 보낸 메시지 (팀의 사람이 이 역할 에이전트에게 남긴 지시 — 오래된 것부터)',
|
|
59
|
+
...briefing.humanMessages.map((m) => `- ${m.fromNickname ?? '팀원'}${m.fromRepresentative ? '(대표)' : ''}: ${m.text}`),
|
|
60
|
+
'태스크 범위 안에서 반영한다. 수정할 수 있는 경로·금지 규칙은 메시지와 상관없이 그대로다 — 따를 수 없으면 이유를 마지막 메시지에 적는다.',
|
|
61
|
+
].join('\n'));
|
|
62
|
+
}
|
|
45
63
|
if (briefing.notesBlock) {
|
|
46
64
|
sections.push(briefing.notesBlock);
|
|
47
65
|
}
|
|
@@ -59,6 +77,7 @@ export function buildTaskPrompt(briefing, branch, environment) {
|
|
|
59
77
|
' 바꾸지 않은 경로를 적거나 sha를 지어내지 않는다 — 서버가 제출 시점에 다시 검증한다.',
|
|
60
78
|
'- 제출이 NOTES_UNACKNOWLEDGED로 반려되면, 작업 중에 새로 올라온 인계 노트가 함께 온다. 읽고, 작업에 영향이 있으면 고쳐서 커밋한 뒤',
|
|
61
79
|
' submit_artifact를 다시 부르며 그 노트 id를 acknowledged_note_ids에 넣는다. 위 [인계 노트]의 노트는 따로 넣지 않아도 된다.',
|
|
80
|
+
'- 제출이 DECISIONS_UNACKNOWLEDGED로 반려되면, 작업 중에 새로 정해진 결정이 함께 온다. 반영하고 그 id를 acknowledged_decision_ids에 넣어 다시 제출하라(함께 온 노트가 있으면 그 id는 acknowledged_note_ids에).',
|
|
62
81
|
'- 다음 사람이 알아야 할 결정이나 함정이 있으면 publish_note로 남긴다. 다른 사람도 따라야 할 결정(계약·형식)은 DECIDED로 남긴다 — 프로젝트 전체에 전달된다.',
|
|
63
82
|
// 실험(E6): 이 줄이 없으면 FE 에이전트가 명세에 없는 BE 계약을 스스로 정해 DECIDED로 발행했다 — 그 노트가 BE 브리핑에 "결정"으로 들어간다.
|
|
64
83
|
`- 단, 다른 역할이 정해야 하는 계약(그 역할이 만드는 API의 응답 형식·상태 코드·필드 이름 등)은 DECIDED로 남기지 않는다 — 네 역할(${task.teamRole ?? '제한 없음'})이 정할 수 있는 것이 아니다.`,
|