@capstone-nomos/cli 0.1.14 → 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
- export function readBriefingNoteIds(workspaceDir, taskId) {
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
- if (parsed.taskId !== taskId || !Array.isArray(parsed.noteIds))
19
+ const ids = parsed[key];
20
+ if (parsed.taskId !== taskId || !Array.isArray(ids))
19
21
  return [];
20
- return parsed.noteIds.filter((id) => typeof id === 'string');
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 { mergeAcknowledged, readBriefingNoteIds, readBriefingTaskId } from './briefing-notes.js';
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
- // 브리핑으로 프롬프트에 들어간 노트는 이미 받은 것이다 — Executor가 작업공간에 남긴 id를 함께 보낸다.
99
- const acknowledgedNoteIds = mergeAcknowledged(readBriefingNoteIds(workspaceDir, task_id), acknowledged_note_ids);
100
- const artifact = await client.submitArtifact(task_id, {
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
- ...(acknowledgedNoteIds.length === 0 ? {} : { acknowledgedNoteIds }),
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, input) {
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이 돌아온다.
@@ -190,8 +190,8 @@ async function handleTask(client, projectId, task) {
190
190
  });
191
191
  log(`작업공간 ${workspace.dir} (브랜치 ${workspace.branch})`);
192
192
  activeWorkspaces.set(task.id, { dir: workspace.dir, repo: briefing.repo.fullName });
193
- // 프롬프트에 넣는 노트 = 제출 때 "받은 노트"로 함께 보낼 노트. MCP 서버(submit_artifact)가 읽는다.
194
- 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));
195
195
  if (workspace.createdBranch) {
196
196
  await client.reportBranch(task.id, workspace.branch);
197
197
  }
@@ -42,6 +42,16 @@ 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
+ }
45
55
  // 사람(그 역할 담당·대표)이 룸에서 보낸 메시지 — 이번 실행이 시작되기 전에 온 것만 들어온다(실행 중에는 끼워 넣지 않는다).
46
56
  if (briefing.humanMessages && briefing.humanMessages.length > 0) {
47
57
  sections.push([
@@ -67,6 +77,7 @@ export function buildTaskPrompt(briefing, branch, environment) {
67
77
  ' 바꾸지 않은 경로를 적거나 sha를 지어내지 않는다 — 서버가 제출 시점에 다시 검증한다.',
68
78
  '- 제출이 NOTES_UNACKNOWLEDGED로 반려되면, 작업 중에 새로 올라온 인계 노트가 함께 온다. 읽고, 작업에 영향이 있으면 고쳐서 커밋한 뒤',
69
79
  ' submit_artifact를 다시 부르며 그 노트 id를 acknowledged_note_ids에 넣는다. 위 [인계 노트]의 노트는 따로 넣지 않아도 된다.',
80
+ '- 제출이 DECISIONS_UNACKNOWLEDGED로 반려되면, 작업 중에 새로 정해진 결정이 함께 온다. 반영하고 그 id를 acknowledged_decision_ids에 넣어 다시 제출하라(함께 온 노트가 있으면 그 id는 acknowledged_note_ids에).',
70
81
  '- 다음 사람이 알아야 할 결정이나 함정이 있으면 publish_note로 남긴다. 다른 사람도 따라야 할 결정(계약·형식)은 DECIDED로 남긴다 — 프로젝트 전체에 전달된다.',
71
82
  // 실험(E6): 이 줄이 없으면 FE 에이전트가 명세에 없는 BE 계약을 스스로 정해 DECIDED로 발행했다 — 그 노트가 BE 브리핑에 "결정"으로 들어간다.
72
83
  `- 단, 다른 역할이 정해야 하는 계약(그 역할이 만드는 API의 응답 형식·상태 코드·필드 이름 등)은 DECIDED로 남기지 않는다 — 네 역할(${task.teamRole ?? '제한 없음'})이 정할 수 있는 것이 아니다.`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capstone-nomos/cli",
3
- "version": "0.1.14",
3
+ "version": "0.1.15",
4
4
  "description": "NOMOS 에이전트 CLI — 노트북의 Claude Code를 NOMOS 서버에 연결하고 배정된 태스크를 실행한다",
5
5
  "type": "module",
6
6
  "bin": {