burhan-mop 0.1.5 → 0.1.7
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/.MOP/PROTOCOL.md +85 -8
- package/.MOP/STATE.json +37 -1
- package/.MOP/scripts/mop-autosycn.mjs +173 -14
- package/.MOP/scripts/mop-core.mjs +289 -16
- package/.agents/AGENTS.md +11 -0
- package/AGENTS.md +14 -2
- package/CLAUDE.md +12 -0
- package/GEMINI.md +10 -0
- package/README.bm.md +1 -1
- package/README.md +1 -1
- package/package.json +1 -1
package/.MOP/PROTOCOL.md
CHANGED
|
@@ -31,7 +31,8 @@ Use this order:
|
|
|
31
31
|
7. Coding/adventure language.
|
|
32
32
|
8. GitHub project link. Required for `team`, optional for `solo`.
|
|
33
33
|
9. GitHub username.
|
|
34
|
-
10. Git commit email.
|
|
34
|
+
10. Git commit email. Default to `github-noreply` so MOP derives the active
|
|
35
|
+
GitHub account email as `ID+USERNAME@users.noreply.github.com`.
|
|
35
36
|
11. If `team`, ask join mode: `open`, `owner-approved`, or `invite`.
|
|
36
37
|
12. Ask auto deploy: `Nak aktifkan auto deploy sekarang? Pilih provider:
|
|
37
38
|
GitHub, Docker, Vercel.` If the user says later/no, answer with the defer
|
|
@@ -40,7 +41,7 @@ Use this order:
|
|
|
40
41
|
After confirmation, run:
|
|
41
42
|
|
|
42
43
|
```bash
|
|
43
|
-
node .MOP/scripts/mop-core.mjs setup --project-name "<name>" --name "<display>" --codename <codename> --password "<password>" --mode <solo|team> --conversation-language "<lang>" --coding-language "<lang>" --git-email
|
|
44
|
+
node .MOP/scripts/mop-core.mjs setup --project-name "<name>" --name "<display>" --codename <codename> --password "<password>" --mode <solo|team> --conversation-language "<lang>" --coding-language "<lang>" --git-email github-noreply [--git-name "<display>"] [--github-username "<github-login>"] [--github-url "<url>"] [--join-mode <mode>]
|
|
44
45
|
```
|
|
45
46
|
|
|
46
47
|
## Agent Naming Ceremony
|
|
@@ -60,6 +61,40 @@ Gate order:
|
|
|
60
61
|
3. `AGENT_GATE`
|
|
61
62
|
4. `ACTION`
|
|
62
63
|
|
|
64
|
+
## Answer Contract
|
|
65
|
+
|
|
66
|
+
For every authenticated user-facing answer, the assistant must show the active
|
|
67
|
+
named agent before the content. This applies to short answers, explanations,
|
|
68
|
+
status updates, plans, code-change summaries, and final responses.
|
|
69
|
+
|
|
70
|
+
Required first line format:
|
|
71
|
+
|
|
72
|
+
```text
|
|
73
|
+
agent: <agent-name> (<agent-role>) to <user>
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Rules:
|
|
77
|
+
|
|
78
|
+
- Do not answer authenticated work without an active named agent.
|
|
79
|
+
- If no active agent exists, ask the user to name the required agent and stop.
|
|
80
|
+
- After `agent route`, use `answerContract.firstLine` from the JSON response.
|
|
81
|
+
- Party Mode keeps the full party dialogue format, but the final handoff to the
|
|
82
|
+
user must still show the speaking agent.
|
|
83
|
+
- Never hide the selected agent in prose. The first visible line must identify
|
|
84
|
+
the agent.
|
|
85
|
+
|
|
86
|
+
Before answering, restore monthly memory:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
node .MOP/scripts/mop-core.mjs memory brief --actor <codename>
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
After meaningful work or a useful answer, save memory:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
node .MOP/scripts/mop-core.mjs memory add --actor <codename> --kind conversation --summary "<one-line outcome>"
|
|
96
|
+
```
|
|
97
|
+
|
|
63
98
|
## Agent Router
|
|
64
99
|
|
|
65
100
|
After authentication and before the Agent Gate, route the user's task to one
|
|
@@ -88,6 +123,9 @@ Routing rules:
|
|
|
88
123
|
genuinely requires several areas of expertise.
|
|
89
124
|
- If the route reports `partyMode.active: true`, run Party Mode before the final
|
|
90
125
|
answer. Name any missing participant agents first.
|
|
126
|
+
- The route JSON includes an `answerContract`. The assistant must restore
|
|
127
|
+
monthly memory, start the visible answer with `answerContract.firstLine`, and
|
|
128
|
+
save a one-line memory after meaningful work.
|
|
91
129
|
|
|
92
130
|
Example:
|
|
93
131
|
|
|
@@ -282,6 +320,34 @@ The skill must call `mop-workflow.mjs help` and answer with the next action,
|
|
|
282
320
|
lead agent, party agents, next artifact, and whether readiness/adversarial gates
|
|
283
321
|
apply.
|
|
284
322
|
|
|
323
|
+
## Monthly Memory
|
|
324
|
+
|
|
325
|
+
MOP keeps durable memory in two layers:
|
|
326
|
+
|
|
327
|
+
- `.MOP/STATE.json` ledger for compact structured state.
|
|
328
|
+
- `.MOP/memory/YYYY-MM.jsonl` for append-only monthly conversation memory.
|
|
329
|
+
|
|
330
|
+
The monthly memory file exists so a fresh chat can restore prior context without
|
|
331
|
+
the user explaining the whole project again. Each useful session must:
|
|
332
|
+
|
|
333
|
+
1. Run `memory brief` before answering after authentication.
|
|
334
|
+
2. Save one-line outcomes with `memory add`.
|
|
335
|
+
3. Use `.MOP/memory/SESSION_BRIEF.md` as the short human-readable handoff.
|
|
336
|
+
|
|
337
|
+
Commands:
|
|
338
|
+
|
|
339
|
+
```bash
|
|
340
|
+
node .MOP/scripts/mop-core.mjs memory brief --actor <codename>
|
|
341
|
+
node .MOP/scripts/mop-core.mjs memory restore --actor <codename>
|
|
342
|
+
node .MOP/scripts/mop-core.mjs memory add --actor <codename> --kind conversation --summary "<what happened>"
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
Autosycn memory also writes to the monthly memory file:
|
|
346
|
+
|
|
347
|
+
```bash
|
|
348
|
+
node .MOP/scripts/mop-autosycn.mjs memory --actor <codename> --summary "<what changed>"
|
|
349
|
+
```
|
|
350
|
+
|
|
285
351
|
## Installer
|
|
286
352
|
|
|
287
353
|
The package installer command is:
|
|
@@ -366,10 +432,13 @@ In team mode, an agent name is the identity.
|
|
|
366
432
|
Autosycn is always available and should be used after meaningful state or file
|
|
367
433
|
changes. It is intentionally identity-safe.
|
|
368
434
|
|
|
369
|
-
Before first push for a member, configure the real
|
|
435
|
+
Before first push for a member, configure the real GitHub identity. Default to
|
|
436
|
+
GitHub noreply so commits attach to the active user account without exposing
|
|
437
|
+
private email:
|
|
370
438
|
|
|
371
439
|
```bash
|
|
372
|
-
|
|
440
|
+
gh auth login
|
|
441
|
+
node .MOP/scripts/mop-core.mjs member git-identity --actor <codename> --name "<display name>" --email github-noreply --github-username "<github-login>"
|
|
373
442
|
```
|
|
374
443
|
|
|
375
444
|
Then run:
|
|
@@ -390,6 +459,13 @@ Autosycn must:
|
|
|
390
459
|
- Commit with `GIT_AUTHOR_NAME`, `GIT_AUTHOR_EMAIL`, `GIT_COMMITTER_NAME`, and
|
|
391
460
|
`GIT_COMMITTER_EMAIL` set from member state.
|
|
392
461
|
- Set local `git config user.name` and `user.email` before commit/merge.
|
|
462
|
+
- For member commits, derive `user.email` from the active GitHub CLI account as
|
|
463
|
+
`ID+USERNAME@users.noreply.github.com` when
|
|
464
|
+
`autosync.githubIdentity.useNoreplyForMemberCommits` is enabled.
|
|
465
|
+
- Refuse to commit or push if `gh api user` is authenticated as a different
|
|
466
|
+
GitHub username than the active member's configured `githubUsername`.
|
|
467
|
+
- Use the member GitHub identity for setup, preflight, memory, and work branch
|
|
468
|
+
commits. Use `BURHAN-MOP` only for merge guardian commits.
|
|
393
469
|
- In team mode, `main` is the trunk and each user works on `dev/<codename>`.
|
|
394
470
|
- Push to `dev/<codename>` in team mode and `main` in solo mode.
|
|
395
471
|
- After a team push, BURHAN-MOP reviews `dev/<codename>` and merges it into
|
|
@@ -404,10 +480,11 @@ Autosycn must:
|
|
|
404
480
|
- If `githubUsername` is configured, refuse to push unless `gh api user`
|
|
405
481
|
verifies the same account.
|
|
406
482
|
|
|
407
|
-
Important: GitHub commit attribution comes from commit email.
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
483
|
+
Important: GitHub commit attribution comes from commit email. MOP uses the
|
|
484
|
+
active GitHub account noreply email for member commits by default. GitHub push
|
|
485
|
+
actor comes from the credential or SSH key used by `git push`; no script can
|
|
486
|
+
fake that. If GitHub shows the wrong pusher, fix `gh auth login`, Git
|
|
487
|
+
Credential Manager, or the SSH key account.
|
|
411
488
|
|
|
412
489
|
## Default Skill: auto-deploy
|
|
413
490
|
|
package/.MOP/STATE.json
CHANGED
|
@@ -13,6 +13,31 @@
|
|
|
13
13
|
"defaultTitle": "Core Agent",
|
|
14
14
|
"gateOrder": "AUTH_GATE_THEN_AGENT_ROUTER_THEN_AGENT_GATE_THEN_ACTION"
|
|
15
15
|
},
|
|
16
|
+
"answerPolicy": {
|
|
17
|
+
"requireVisibleAgent": true,
|
|
18
|
+
"visibleAgentFormat": "agent: <agent-name> (<agent-role>) to <user>",
|
|
19
|
+
"requireMemoryRestore": true,
|
|
20
|
+
"requireMemorySave": true,
|
|
21
|
+
"rules": [
|
|
22
|
+
"Every authenticated user-facing answer must start with the active named agent line.",
|
|
23
|
+
"If no active named agent exists, ask the user to name the required agent before answering the task.",
|
|
24
|
+
"Party Mode keeps its dialogue format, but the final handoff to the user must still show the speaking agent."
|
|
25
|
+
]
|
|
26
|
+
},
|
|
27
|
+
"memoryPolicy": {
|
|
28
|
+
"enabled": true,
|
|
29
|
+
"directory": ".MOP/memory",
|
|
30
|
+
"sessionBrief": ".MOP/memory/SESSION_BRIEF.md",
|
|
31
|
+
"monthlyPattern": "YYYY-MM.jsonl",
|
|
32
|
+
"recentLimit": 20,
|
|
33
|
+
"restoreBeforeAnswer": true,
|
|
34
|
+
"saveAfterMeaningfulWork": true,
|
|
35
|
+
"rules": [
|
|
36
|
+
"Save one-line memories to a monthly JSONL file so new chats can restore context.",
|
|
37
|
+
"Generate SESSION_BRIEF.md from recent monthly memory entries.",
|
|
38
|
+
"Read memory brief before answering any authenticated task."
|
|
39
|
+
]
|
|
40
|
+
},
|
|
16
41
|
"agentRouter": {
|
|
17
42
|
"enabled": true,
|
|
18
43
|
"primaryAgentLimit": 1,
|
|
@@ -374,7 +399,18 @@
|
|
|
374
399
|
}
|
|
375
400
|
},
|
|
376
401
|
"requireUserGitEmail": true,
|
|
377
|
-
"verifyGhUserWhenConfigured": true
|
|
402
|
+
"verifyGhUserWhenConfigured": true,
|
|
403
|
+
"githubIdentity": {
|
|
404
|
+
"requireMatchedGhUser": true,
|
|
405
|
+
"useNoreplyForMemberCommits": true,
|
|
406
|
+
"noreplyFormat": "id-plus-login",
|
|
407
|
+
"rules": [
|
|
408
|
+
"Member commits must use the active member GitHub account, not an AI or guardian identity.",
|
|
409
|
+
"When gh is authenticated, derive member commit email as ID+USERNAME@users.noreply.github.com.",
|
|
410
|
+
"Refuse autosycn if the active gh account does not match the member githubUsername.",
|
|
411
|
+
"BURHAN-MOP identity is reserved for merge guardian commits only."
|
|
412
|
+
]
|
|
413
|
+
}
|
|
378
414
|
},
|
|
379
415
|
"members": {},
|
|
380
416
|
"installedSkills": [
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { existsSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import { dirname, join, resolve } from 'node:path';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
5
|
import { spawnSync } from 'node:child_process';
|
|
@@ -109,6 +109,123 @@ function agentLedgerFields(agent) {
|
|
|
109
109
|
} : {};
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
+
function memoryPolicy(state) {
|
|
113
|
+
return state.memoryPolicy || {
|
|
114
|
+
enabled: true,
|
|
115
|
+
directory: '.MOP/memory',
|
|
116
|
+
sessionBrief: '.MOP/memory/SESSION_BRIEF.md',
|
|
117
|
+
monthlyPattern: 'YYYY-MM.jsonl',
|
|
118
|
+
recentLimit: 20
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function answerPolicy(state) {
|
|
123
|
+
return state.answerPolicy || {
|
|
124
|
+
requireVisibleAgent: true,
|
|
125
|
+
visibleAgentFormat: 'agent: <agent-name> (<agent-role>) to <user>'
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function monthKey(date = new Date()) {
|
|
130
|
+
return date.toISOString().slice(0, 7);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function memoryDirFor(state) {
|
|
134
|
+
return join(rootDir, memoryPolicy(state).directory || '.MOP/memory');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function monthlyMemoryPath(state, month = monthKey()) {
|
|
138
|
+
const policy = memoryPolicy(state);
|
|
139
|
+
const filename = (policy.monthlyPattern || 'YYYY-MM.jsonl').replace('YYYY-MM', month);
|
|
140
|
+
return join(memoryDirFor(state), filename);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function sessionBriefPath(state) {
|
|
144
|
+
return join(rootDir, memoryPolicy(state).sessionBrief || '.MOP/memory/SESSION_BRIEF.md');
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function readJsonl(path) {
|
|
148
|
+
if (!existsSync(path)) return [];
|
|
149
|
+
return readFileSync(path, 'utf8')
|
|
150
|
+
.split(/\r?\n/)
|
|
151
|
+
.filter(Boolean)
|
|
152
|
+
.map((line) => {
|
|
153
|
+
try {
|
|
154
|
+
return JSON.parse(line);
|
|
155
|
+
} catch {
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
})
|
|
159
|
+
.filter(Boolean);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function memoryMonths(state) {
|
|
163
|
+
const dir = memoryDirFor(state);
|
|
164
|
+
if (!existsSync(dir)) return [];
|
|
165
|
+
return readdirSync(dir)
|
|
166
|
+
.filter((name) => /^\d{4}-\d{2}\.jsonl$/.test(name))
|
|
167
|
+
.map((name) => name.replace(/\.jsonl$/, ''))
|
|
168
|
+
.sort();
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function latestMemoryEntries(state, limit = memoryPolicy(state).recentLimit || 20) {
|
|
172
|
+
const months = memoryMonths(state);
|
|
173
|
+
const selected = months.length ? months.slice(-3) : [monthKey()];
|
|
174
|
+
return selected
|
|
175
|
+
.flatMap((month) => readJsonl(monthlyMemoryPath(state, month)))
|
|
176
|
+
.sort((a, b) => String(a.at || '').localeCompare(String(b.at || '')))
|
|
177
|
+
.slice(-limit);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function answerLineFor(state, actor, agent = activeAgentFor(state, actor)) {
|
|
181
|
+
if (!agent) return 'agent: <name> (<role>) to <user>';
|
|
182
|
+
return (answerPolicy(state).visibleAgentFormat || 'agent: <agent-name> (<agent-role>) to <user>')
|
|
183
|
+
.replace('<agent-name>', agent.name)
|
|
184
|
+
.replace('<agent-role>', agent.role)
|
|
185
|
+
.replace('<agent-title>', agent.title || agent.role)
|
|
186
|
+
.replace('<user>', actor || 'user');
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function appendMonthlyMemory(state, actor, kind, summary, agent = activeAgentFor(state, actor)) {
|
|
190
|
+
if (memoryPolicy(state).enabled === false) return;
|
|
191
|
+
const entry = { at: now(), actor, ...agentLedgerFields(agent), kind, summary };
|
|
192
|
+
const monthlyPath = monthlyMemoryPath(state);
|
|
193
|
+
mkdirSync(dirname(monthlyPath), { recursive: true });
|
|
194
|
+
writeFileSync(monthlyPath, `${JSON.stringify(entry)}\n`, { encoding: 'utf8', flag: 'a' });
|
|
195
|
+
writeSessionBrief(state, actor);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function writeSessionBrief(state, actor) {
|
|
199
|
+
const path = sessionBriefPath(state);
|
|
200
|
+
const agent = activeAgentFor(state, actor);
|
|
201
|
+
const entries = latestMemoryEntries(state, memoryPolicy(state).recentLimit || 20);
|
|
202
|
+
const lines = [
|
|
203
|
+
'# MOP Session Brief',
|
|
204
|
+
'',
|
|
205
|
+
`Updated: ${now()}`,
|
|
206
|
+
`Actor: ${actor || state.activeMember || 'unknown'}`,
|
|
207
|
+
`Active agent: ${agent ? `${agent.name} (${agent.role})` : 'none'}`,
|
|
208
|
+
`Current month: ${monthKey()}`,
|
|
209
|
+
'',
|
|
210
|
+
'## Required Session Flow',
|
|
211
|
+
'',
|
|
212
|
+
'1. Read `.MOP/STATE.json` and follow `.MOP/PROTOCOL.md`.',
|
|
213
|
+
'2. Restore memory with `node .MOP/scripts/mop-core.mjs memory brief --actor <codename>`.',
|
|
214
|
+
'3. Run `agent route` for the user task before answering.',
|
|
215
|
+
`4. Start every authenticated answer with: \`${answerLineFor(state, actor, agent)}\``,
|
|
216
|
+
'5. Save a one-line memory after meaningful work.',
|
|
217
|
+
'',
|
|
218
|
+
'## Recent Memory',
|
|
219
|
+
'',
|
|
220
|
+
...entries.map((entry) => {
|
|
221
|
+
const who = entry.agent ? `${entry.agent} (${entry.agentRole || 'agent'})` : entry.actor;
|
|
222
|
+
return `- ${entry.at} - ${who}: ${entry.summary}`;
|
|
223
|
+
})
|
|
224
|
+
];
|
|
225
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
226
|
+
writeFileSync(path, `${lines.join('\n')}\n`, 'utf8');
|
|
227
|
+
}
|
|
228
|
+
|
|
112
229
|
function appendLedger(state, actor, kind, summary, agent = activeAgentFor(state, actor)) {
|
|
113
230
|
state.ledger ||= [];
|
|
114
231
|
state.ledger.push({ at: now(), actor, ...agentLedgerFields(agent), kind, summary });
|
|
@@ -130,22 +247,59 @@ function getMember(state, actor) {
|
|
|
130
247
|
return member;
|
|
131
248
|
}
|
|
132
249
|
|
|
250
|
+
function githubIdentityPolicy(state) {
|
|
251
|
+
return state.autosync?.githubIdentity || {
|
|
252
|
+
requireMatchedGhUser: true,
|
|
253
|
+
useNoreplyForMemberCommits: true,
|
|
254
|
+
noreplyFormat: 'id-plus-login'
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function currentGhUser() {
|
|
259
|
+
const result = runOptional('gh', ['api', 'user', '--jq', '{login:.login,id:.id,email:.email}']);
|
|
260
|
+
if (!result.ok) return null;
|
|
261
|
+
try {
|
|
262
|
+
return JSON.parse(result.stdout || '{}');
|
|
263
|
+
} catch {
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function githubNoreplyEmail(user) {
|
|
269
|
+
if (!user?.login || !user?.id) return '';
|
|
270
|
+
return `${user.id}+${user.login}@users.noreply.github.com`;
|
|
271
|
+
}
|
|
272
|
+
|
|
133
273
|
function identityFor(state, actor) {
|
|
134
274
|
const member = getMember(state, actor);
|
|
135
275
|
const identity = member.gitIdentity || {};
|
|
276
|
+
const policy = githubIdentityPolicy(state);
|
|
277
|
+
const gh = currentGhUser();
|
|
136
278
|
const name = identity.name || member.displayName || actor;
|
|
137
|
-
const
|
|
279
|
+
const githubUsername = identity.githubUsername || member.github?.username || gh?.login || '';
|
|
280
|
+
if (gh?.login && githubUsername && policy.requireMatchedGhUser !== false && gh.login.toLowerCase() !== githubUsername.toLowerCase()) {
|
|
281
|
+
throw new Error(`GitHub CLI authenticated as ${gh.login}, expected ${githubUsername}. Refusing to commit or push as the wrong user.`);
|
|
282
|
+
}
|
|
283
|
+
let email = identity.email || member.github?.noreplyEmail || '';
|
|
284
|
+
if (policy.useNoreplyForMemberCommits !== false) {
|
|
285
|
+
email = githubNoreplyEmail(gh);
|
|
286
|
+
if (!email) {
|
|
287
|
+
throw new Error('GitHub noreply identity is required for member commits. Run gh auth login as the real user, or set autosync.githubIdentity.useNoreplyForMemberCommits=false.');
|
|
288
|
+
}
|
|
289
|
+
}
|
|
138
290
|
if (!email && state.autosync?.requireUserGitEmail !== false) {
|
|
139
291
|
throw new Error([
|
|
140
292
|
`Missing git email for ${actor}.`,
|
|
141
|
-
'Set a GitHub-verified email or noreply
|
|
142
|
-
`node .MOP/scripts/mop-core.mjs member git-identity --actor ${actor} --name "${name}" --email
|
|
293
|
+
'Set a GitHub-verified email or let MOP derive GitHub noreply from gh:',
|
|
294
|
+
`node .MOP/scripts/mop-core.mjs member git-identity --actor ${actor} --name "${name}" --email github-noreply [--github-username "<username>"]`
|
|
143
295
|
].join(' '));
|
|
144
296
|
}
|
|
145
297
|
return {
|
|
146
298
|
name,
|
|
147
299
|
email,
|
|
148
|
-
githubUsername
|
|
300
|
+
githubUsername,
|
|
301
|
+
githubUserId: gh?.login?.toLowerCase() === githubUsername.toLowerCase() ? gh.id : identity.githubUserId,
|
|
302
|
+
emailSource: policy.useNoreplyForMemberCommits !== false ? 'github-noreply' : (identity.emailSource || 'manual')
|
|
149
303
|
};
|
|
150
304
|
}
|
|
151
305
|
|
|
@@ -218,15 +372,16 @@ function configureRemote(url, replaceRemote = false) {
|
|
|
218
372
|
}
|
|
219
373
|
|
|
220
374
|
function verifyGhUser(identity, state) {
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
375
|
+
const policy = githubIdentityPolicy(state);
|
|
376
|
+
if (state.autosync?.verifyGhUserWhenConfigured === false && policy.requireMatchedGhUser === false) return 'skipped';
|
|
377
|
+
const gh = currentGhUser();
|
|
378
|
+
if (!gh?.login) {
|
|
224
379
|
throw new Error('GitHub username is configured, but gh could not verify the active account. Run gh auth login as the real user or set autosync.verifyGhUserWhenConfigured=false for SSH-only workflows.');
|
|
225
380
|
}
|
|
226
|
-
if (gh.
|
|
227
|
-
throw new Error(`GitHub CLI authenticated as ${gh.
|
|
381
|
+
if (identity.githubUsername && gh.login.toLowerCase() !== identity.githubUsername.toLowerCase()) {
|
|
382
|
+
throw new Error(`GitHub CLI authenticated as ${gh.login}, expected ${identity.githubUsername}. Refusing to push as the wrong account.`);
|
|
228
383
|
}
|
|
229
|
-
return `verified:${gh.
|
|
384
|
+
return `verified:${gh.login}:${githubNoreplyEmail(gh) || 'email-unavailable'}`;
|
|
230
385
|
}
|
|
231
386
|
|
|
232
387
|
function runProjectCommand(command, env) {
|
|
@@ -392,10 +547,11 @@ function preflight(args) {
|
|
|
392
547
|
}, null, 2));
|
|
393
548
|
}
|
|
394
549
|
|
|
395
|
-
function saveMemory(actor, summary) {
|
|
550
|
+
function saveMemory(actor, summary, kind = 'conversation') {
|
|
396
551
|
const state = readState();
|
|
397
552
|
const agent = requireActiveAgent(state, actor);
|
|
398
553
|
appendLedger(state, actor, 'memory', summary, agent);
|
|
554
|
+
appendMonthlyMemory(state, actor, kind, summary, agent);
|
|
399
555
|
writeState(state);
|
|
400
556
|
return state;
|
|
401
557
|
}
|
|
@@ -559,6 +715,7 @@ function status() {
|
|
|
559
715
|
workBranchPrefix: state.autosync?.workBranchPrefix || 'dev',
|
|
560
716
|
autoMergeToMain: state.autosync?.autoMergeToMain !== false,
|
|
561
717
|
mergeGuardian: guardianConfig(state),
|
|
718
|
+
githubIdentity: githubIdentityPolicy(state),
|
|
562
719
|
preflightBeforeWork: state.autosync?.preflightBeforeWork !== false,
|
|
563
720
|
requireUserGitEmail: state.autosync?.requireUserGitEmail !== false,
|
|
564
721
|
initialized: state.initialized,
|
|
@@ -573,7 +730,8 @@ function status() {
|
|
|
573
730
|
displayName: member.displayName,
|
|
574
731
|
gitIdentityConfigured: Boolean(member.gitIdentity?.email || member.github?.noreplyEmail),
|
|
575
732
|
gitName: member.gitIdentity?.name || member.displayName || key,
|
|
576
|
-
gitEmail: member.gitIdentity?.email || member.github?.noreplyEmail || ''
|
|
733
|
+
gitEmail: member.gitIdentity?.email || member.github?.noreplyEmail || '',
|
|
734
|
+
githubUsername: member.gitIdentity?.githubUsername || member.github?.username || ''
|
|
577
735
|
}
|
|
578
736
|
]))
|
|
579
737
|
}, null, 2));
|
|
@@ -588,7 +746,8 @@ function main() {
|
|
|
588
746
|
if (command === 'memory') {
|
|
589
747
|
const actor = requireArg(args, 'actor');
|
|
590
748
|
const summary = String(args.summary || args.reason || 'MOP conversation');
|
|
591
|
-
|
|
749
|
+
const kind = String(args.kind || 'conversation');
|
|
750
|
+
saveMemory(actor, summary, kind);
|
|
592
751
|
console.log(`Memory saved for ${actor}.`);
|
|
593
752
|
return;
|
|
594
753
|
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import { dirname, join, resolve } from 'node:path';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
5
|
import { randomBytes, scryptSync, timingSafeEqual } from 'node:crypto';
|
|
6
|
+
import { spawnSync } from 'node:child_process';
|
|
6
7
|
|
|
7
8
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
8
9
|
const coreDir = resolve(here, '..');
|
|
@@ -71,6 +72,53 @@ function verifyPassword(password, salt, expectedHex) {
|
|
|
71
72
|
return expected.length === actual.length && timingSafeEqual(actual, expected);
|
|
72
73
|
}
|
|
73
74
|
|
|
75
|
+
function currentGhUser() {
|
|
76
|
+
const result = spawnSync('gh', ['api', 'user', '--jq', '{login:.login,id:.id,email:.email}'], {
|
|
77
|
+
cwd: rootDir,
|
|
78
|
+
encoding: 'utf8'
|
|
79
|
+
});
|
|
80
|
+
if (result.status !== 0) return null;
|
|
81
|
+
try {
|
|
82
|
+
return JSON.parse(result.stdout || '{}');
|
|
83
|
+
} catch {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function githubNoreplyEmail(user) {
|
|
89
|
+
if (!user?.login || !user?.id) return '';
|
|
90
|
+
return `${user.id}+${user.login}@users.noreply.github.com`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function resolveGitIdentityInput(state, actor, name, emailInput, githubUsernameInput) {
|
|
94
|
+
const policy = state.autosync?.githubIdentity || {};
|
|
95
|
+
const preferNoreply = policy.useNoreplyForMemberCommits !== false;
|
|
96
|
+
const gh = currentGhUser();
|
|
97
|
+
const githubUsername = githubUsernameInput || gh?.login || '';
|
|
98
|
+
if (gh?.login && githubUsername && policy.requireMatchedGhUser !== false && gh.login.toLowerCase() !== githubUsername.toLowerCase()) {
|
|
99
|
+
throw new Error(`GitHub CLI authenticated as ${gh.login}, expected ${githubUsername}. Run gh auth login as the real user.`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
let email = String(emailInput || '').trim();
|
|
103
|
+
const wantsNoreply = !email || ['auto', 'github', 'github-noreply', 'noreply'].includes(email.toLowerCase());
|
|
104
|
+
if (preferNoreply && wantsNoreply) {
|
|
105
|
+
email = githubNoreplyEmail(gh);
|
|
106
|
+
if (!email) {
|
|
107
|
+
throw new Error('Cannot derive GitHub noreply email. Run gh auth login or provide --git-email "<github-verified-email>".');
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (!email && state.autosync?.requireUserGitEmail !== false) {
|
|
111
|
+
throw new Error('Git email is required. Use --git-email github-noreply after gh auth login, or provide a GitHub-verified email.');
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
name,
|
|
115
|
+
email,
|
|
116
|
+
githubUsername,
|
|
117
|
+
githubUserId: gh?.login?.toLowerCase() === githubUsername.toLowerCase() ? gh.id : undefined,
|
|
118
|
+
emailSource: wantsNoreply && email ? 'github-noreply' : 'manual'
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
74
122
|
function activeAgentFor(state, actor) {
|
|
75
123
|
const activeId = state.activeAgents?.[actor];
|
|
76
124
|
if (!activeId) return null;
|
|
@@ -87,6 +135,103 @@ function agentLedgerFields(agent) {
|
|
|
87
135
|
} : {};
|
|
88
136
|
}
|
|
89
137
|
|
|
138
|
+
function memoryPolicy(state) {
|
|
139
|
+
return state.memoryPolicy || {
|
|
140
|
+
enabled: true,
|
|
141
|
+
directory: '.MOP/memory',
|
|
142
|
+
sessionBrief: '.MOP/memory/SESSION_BRIEF.md',
|
|
143
|
+
monthlyPattern: 'YYYY-MM.jsonl',
|
|
144
|
+
recentLimit: 20
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function answerPolicy(state) {
|
|
149
|
+
return state.answerPolicy || {
|
|
150
|
+
requireVisibleAgent: true,
|
|
151
|
+
visibleAgentFormat: 'agent: <agent-name> (<agent-role>) to <user>',
|
|
152
|
+
requireMemoryRestore: true,
|
|
153
|
+
requireMemorySave: true
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function monthKey(date = new Date()) {
|
|
158
|
+
return date.toISOString().slice(0, 7);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function relativeFromRoot(path) {
|
|
162
|
+
return path.replace(rootDir, '').replace(/^[\\/]/, '').replaceAll('\\', '/');
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function memoryDirFor(state) {
|
|
166
|
+
return join(rootDir, memoryPolicy(state).directory || '.MOP/memory');
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function monthlyMemoryPath(state, month = monthKey()) {
|
|
170
|
+
const policy = memoryPolicy(state);
|
|
171
|
+
const filename = (policy.monthlyPattern || 'YYYY-MM.jsonl').replace('YYYY-MM', month);
|
|
172
|
+
return join(memoryDirFor(state), filename);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function sessionBriefPath(state) {
|
|
176
|
+
return join(rootDir, memoryPolicy(state).sessionBrief || '.MOP/memory/SESSION_BRIEF.md');
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function readJsonl(path) {
|
|
180
|
+
if (!existsSync(path)) return [];
|
|
181
|
+
return readFileSync(path, 'utf8')
|
|
182
|
+
.split(/\r?\n/)
|
|
183
|
+
.filter(Boolean)
|
|
184
|
+
.map((line) => {
|
|
185
|
+
try {
|
|
186
|
+
return JSON.parse(line);
|
|
187
|
+
} catch {
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
})
|
|
191
|
+
.filter(Boolean);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function memoryMonths(state) {
|
|
195
|
+
const dir = memoryDirFor(state);
|
|
196
|
+
if (!existsSync(dir)) return [];
|
|
197
|
+
return readdirSync(dir)
|
|
198
|
+
.filter((name) => /^\d{4}-\d{2}\.jsonl$/.test(name))
|
|
199
|
+
.map((name) => name.replace(/\.jsonl$/, ''))
|
|
200
|
+
.sort();
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function latestMemoryEntries(state, limit = memoryPolicy(state).recentLimit || 20) {
|
|
204
|
+
const months = memoryMonths(state);
|
|
205
|
+
const selected = months.length ? months.slice(-3) : [monthKey()];
|
|
206
|
+
return selected
|
|
207
|
+
.flatMap((month) => readJsonl(monthlyMemoryPath(state, month)))
|
|
208
|
+
.sort((a, b) => String(a.at || '').localeCompare(String(b.at || '')))
|
|
209
|
+
.slice(-limit);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function answerContractFor(state, actor, agent = activeAgentFor(state, actor)) {
|
|
213
|
+
const policy = answerPolicy(state);
|
|
214
|
+
const format = policy.visibleAgentFormat || 'agent: <agent-name> (<agent-role>) to <user>';
|
|
215
|
+
const firstLine = agent
|
|
216
|
+
? format
|
|
217
|
+
.replace('<agent-name>', agent.name)
|
|
218
|
+
.replace('<agent-role>', agent.role)
|
|
219
|
+
.replace('<agent-title>', agent.title || agent.role)
|
|
220
|
+
.replace('<user>', actor || 'user')
|
|
221
|
+
: '';
|
|
222
|
+
return {
|
|
223
|
+
required: policy.requireVisibleAgent !== false,
|
|
224
|
+
firstLine,
|
|
225
|
+
beforeAnswer: `node .MOP/scripts/mop-core.mjs memory brief --actor ${actor || '<codename>'}`,
|
|
226
|
+
afterAnswer: `node .MOP/scripts/mop-core.mjs memory add --actor ${actor || '<codename>'} --kind conversation --summary "<one-line outcome>"`,
|
|
227
|
+
rules: [
|
|
228
|
+
'Do not answer authenticated work without an active named agent.',
|
|
229
|
+
'Every user-facing answer must show the active agent line first.',
|
|
230
|
+
'Restore monthly memory before answering and save a one-line memory after meaningful work.'
|
|
231
|
+
]
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
90
235
|
function requireActiveAgent(state, actor, role = 'core', title = 'Core Agent') {
|
|
91
236
|
const agent = activeAgentFor(state, actor);
|
|
92
237
|
if (agent) return agent;
|
|
@@ -102,6 +247,48 @@ function appendLedger(state, actor, kind, summary, agent = activeAgentFor(state,
|
|
|
102
247
|
state.ledger.push({ at: now(), actor, ...agentLedgerFields(agent), kind, summary });
|
|
103
248
|
}
|
|
104
249
|
|
|
250
|
+
function appendMonthlyMemory(state, actor, kind, summary, agent = activeAgentFor(state, actor)) {
|
|
251
|
+
if (memoryPolicy(state).enabled === false) return null;
|
|
252
|
+
const entry = { at: now(), actor, ...agentLedgerFields(agent), kind, summary };
|
|
253
|
+
const monthlyPath = monthlyMemoryPath(state);
|
|
254
|
+
mkdirSync(dirname(monthlyPath), { recursive: true });
|
|
255
|
+
writeFileSync(monthlyPath, `${JSON.stringify(entry)}\n`, { encoding: 'utf8', flag: 'a' });
|
|
256
|
+
writeSessionBrief(state, actor);
|
|
257
|
+
return { entry, monthlyPath };
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function writeSessionBrief(state, actor) {
|
|
261
|
+
const path = sessionBriefPath(state);
|
|
262
|
+
const agent = activeAgentFor(state, actor);
|
|
263
|
+
const entries = latestMemoryEntries(state, memoryPolicy(state).recentLimit || 20);
|
|
264
|
+
const contract = answerContractFor(state, actor, agent);
|
|
265
|
+
const lines = [
|
|
266
|
+
'# MOP Session Brief',
|
|
267
|
+
'',
|
|
268
|
+
`Updated: ${now()}`,
|
|
269
|
+
`Actor: ${actor || state.activeMember || 'unknown'}`,
|
|
270
|
+
`Active agent: ${agent ? `${agent.name} (${agent.role})` : 'none'}`,
|
|
271
|
+
`Current month: ${monthKey()}`,
|
|
272
|
+
'',
|
|
273
|
+
'## Required Session Flow',
|
|
274
|
+
'',
|
|
275
|
+
'1. Read `.MOP/STATE.json` and follow `.MOP/PROTOCOL.md`.',
|
|
276
|
+
'2. Authenticate if required.',
|
|
277
|
+
'3. Run `agent route` for the user task before answering.',
|
|
278
|
+
`4. Start every authenticated answer with: \`${contract.firstLine || 'agent: <name> (<role>) to <user>'}\``,
|
|
279
|
+
'5. Save a one-line memory after meaningful work.',
|
|
280
|
+
'',
|
|
281
|
+
'## Recent Memory',
|
|
282
|
+
'',
|
|
283
|
+
...entries.map((entry) => {
|
|
284
|
+
const who = entry.agent ? `${entry.agent} (${entry.agentRole || 'agent'})` : entry.actor;
|
|
285
|
+
return `- ${entry.at} - ${who}: ${entry.summary}`;
|
|
286
|
+
})
|
|
287
|
+
];
|
|
288
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
289
|
+
writeFileSync(path, `${lines.join('\n')}\n`, 'utf8');
|
|
290
|
+
}
|
|
291
|
+
|
|
105
292
|
const routeRules = [
|
|
106
293
|
{
|
|
107
294
|
role: 'memory',
|
|
@@ -363,7 +550,7 @@ function setup(args) {
|
|
|
363
550
|
const codingLanguage = String(args['coding-language'] || 'English');
|
|
364
551
|
const githubUrl = String(args['github-url'] || '');
|
|
365
552
|
const gitName = String(args['git-name'] || displayName);
|
|
366
|
-
const gitEmail = String(args['git-email'] || '');
|
|
553
|
+
const gitEmail = String(args['git-email'] || 'github-noreply');
|
|
367
554
|
const githubUsername = String(args['github-username'] || '');
|
|
368
555
|
const joinMode = String(args['join-mode'] || 'owner-approved');
|
|
369
556
|
|
|
@@ -371,12 +558,10 @@ function setup(args) {
|
|
|
371
558
|
if (password.length < 8) throw new Error('Password must be at least 8 characters.');
|
|
372
559
|
if (!['solo', 'team'].includes(mode)) throw new Error('Mode must be solo or team.');
|
|
373
560
|
if (mode === 'team' && !githubUrl) throw new Error('Team mode requires --github-url.');
|
|
374
|
-
if (state.autosync?.requireUserGitEmail !== false && !gitEmail) {
|
|
375
|
-
throw new Error('Git email is required so commits are attributed to the real user, not the AI tool.');
|
|
376
|
-
}
|
|
377
561
|
if (!['open', 'owner-approved', 'invite'].includes(joinMode)) {
|
|
378
562
|
throw new Error('Join mode must be open, owner-approved, or invite.');
|
|
379
563
|
}
|
|
564
|
+
const gitIdentity = resolveGitIdentityInput(state, codename, gitName, gitEmail, githubUsername);
|
|
380
565
|
|
|
381
566
|
const { passwordHash, passwordSalt } = hashPassword(password);
|
|
382
567
|
state.initialized = true;
|
|
@@ -406,11 +591,7 @@ function setup(args) {
|
|
|
406
591
|
conversation: conversationLanguage,
|
|
407
592
|
coding: codingLanguage
|
|
408
593
|
},
|
|
409
|
-
gitIdentity
|
|
410
|
-
name: gitName,
|
|
411
|
-
email: gitEmail,
|
|
412
|
-
githubUsername
|
|
413
|
-
},
|
|
594
|
+
gitIdentity,
|
|
414
595
|
joinedAt: now()
|
|
415
596
|
}
|
|
416
597
|
};
|
|
@@ -435,6 +616,12 @@ function login(args) {
|
|
|
435
616
|
console.log(`Active member: ${codename}`);
|
|
436
617
|
if (!activeAgentFor(state, codename) && state.agentPolicy?.requiredAfterAuth !== false) {
|
|
437
618
|
console.log(`Agent diperlukan. Jalankan: node .MOP/scripts/mop-core.mjs agent activate --actor ${codename} --role ${state.agentPolicy?.defaultRole || 'core'} --title "${state.agentPolicy?.defaultTitle || 'Core Agent'}" --name "<agent-name>"`);
|
|
619
|
+
} else {
|
|
620
|
+
console.log(JSON.stringify({
|
|
621
|
+
next: 'restore-memory-and-route-task',
|
|
622
|
+
memoryRestore: `node .MOP/scripts/mop-core.mjs memory brief --actor ${codename}`,
|
|
623
|
+
answerContract: answerContractFor(state, codename)
|
|
624
|
+
}, null, 2));
|
|
438
625
|
}
|
|
439
626
|
}
|
|
440
627
|
|
|
@@ -553,6 +740,11 @@ function agentRoute(args) {
|
|
|
553
740
|
route,
|
|
554
741
|
partyAgents,
|
|
555
742
|
activeAgent: null,
|
|
743
|
+
answerContract: null,
|
|
744
|
+
monthlyMemory: {
|
|
745
|
+
restoreCommand: `node .MOP/scripts/mop-core.mjs memory brief --actor ${actor}`,
|
|
746
|
+
saveCommand: `node .MOP/scripts/mop-core.mjs memory add --actor ${actor} --kind conversation --summary "<one-line outcome>"`
|
|
747
|
+
},
|
|
556
748
|
nextAction: null
|
|
557
749
|
};
|
|
558
750
|
|
|
@@ -567,6 +759,7 @@ function agentRoute(args) {
|
|
|
567
759
|
role: agent.role,
|
|
568
760
|
title: agent.title
|
|
569
761
|
};
|
|
762
|
+
response.answerContract = answerContractFor(state, actor, agent);
|
|
570
763
|
if (route.partyMode.active && missingPartyAgents.length) {
|
|
571
764
|
response.ok = false;
|
|
572
765
|
response.nextAction = 'name-required-party-agents';
|
|
@@ -596,6 +789,73 @@ function agentList() {
|
|
|
596
789
|
}, null, 2));
|
|
597
790
|
}
|
|
598
791
|
|
|
792
|
+
function memoryAdd(args) {
|
|
793
|
+
const state = readState();
|
|
794
|
+
if (!state.initialized) throw new Error('MOP is not initialized.');
|
|
795
|
+
const actor = slug(requireArg(args, 'actor'));
|
|
796
|
+
if (!state.members?.[actor]) throw new Error('Unknown actor.');
|
|
797
|
+
const agent = requireActiveAgent(state, actor);
|
|
798
|
+
const summary = String(args.summary || args._?.join(' ') || '').trim();
|
|
799
|
+
const kind = String(args.kind || 'conversation');
|
|
800
|
+
if (!summary) throw new Error('Missing --summary');
|
|
801
|
+
|
|
802
|
+
appendLedger(state, actor, 'memory', summary, agent);
|
|
803
|
+
const saved = appendMonthlyMemory(state, actor, kind, summary, agent);
|
|
804
|
+
writeState(state);
|
|
805
|
+
console.log(JSON.stringify({
|
|
806
|
+
ok: true,
|
|
807
|
+
actor,
|
|
808
|
+
agent: agent.name,
|
|
809
|
+
kind,
|
|
810
|
+
summary,
|
|
811
|
+
monthlyMemory: saved ? relativeFromRoot(saved.monthlyPath) : 'disabled',
|
|
812
|
+
sessionBrief: relativeFromRoot(sessionBriefPath(state)),
|
|
813
|
+
answerContract: answerContractFor(state, actor, agent)
|
|
814
|
+
}, null, 2));
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
function memoryBrief(args) {
|
|
818
|
+
const state = readState();
|
|
819
|
+
if (!state.initialized) {
|
|
820
|
+
console.log('MOP belum di-setup. Jalankan /mop-setup.');
|
|
821
|
+
return;
|
|
822
|
+
}
|
|
823
|
+
const actor = slug(String(args.actor || state.activeMember || ''));
|
|
824
|
+
if (!actor) throw new Error('Missing --actor');
|
|
825
|
+
if (!state.members?.[actor]) throw new Error('Unknown actor.');
|
|
826
|
+
const agent = activeAgentFor(state, actor);
|
|
827
|
+
const month = String(args.month || monthKey());
|
|
828
|
+
const limit = Number(args.limit || memoryPolicy(state).recentLimit || 20);
|
|
829
|
+
const currentEntries = readJsonl(monthlyMemoryPath(state, month)).slice(-limit);
|
|
830
|
+
const recentEntries = latestMemoryEntries(state, limit);
|
|
831
|
+
if (agent) writeSessionBrief(state, actor);
|
|
832
|
+
console.log(JSON.stringify({
|
|
833
|
+
ok: true,
|
|
834
|
+
actor,
|
|
835
|
+
activeAgent: agent ? {
|
|
836
|
+
id: agent.id,
|
|
837
|
+
name: agent.name,
|
|
838
|
+
role: agent.role,
|
|
839
|
+
title: agent.title
|
|
840
|
+
} : null,
|
|
841
|
+
answerContract: answerContractFor(state, actor, agent),
|
|
842
|
+
memory: {
|
|
843
|
+
month,
|
|
844
|
+
monthPath: relativeFromRoot(monthlyMemoryPath(state, month)),
|
|
845
|
+
sessionBrief: relativeFromRoot(sessionBriefPath(state)),
|
|
846
|
+
currentMonthEntries: currentEntries,
|
|
847
|
+
recentEntries
|
|
848
|
+
},
|
|
849
|
+
next: agent
|
|
850
|
+
? 'Use answerContract.firstLine before answering, then save memory after meaningful work.'
|
|
851
|
+
: `Agent diperlukan. Jalankan: node .MOP/scripts/mop-core.mjs agent activate --actor ${actor} --role ${state.agentPolicy?.defaultRole || 'core'} --title "${state.agentPolicy?.defaultTitle || 'Core Agent'}" --name "<agent-name>"`
|
|
852
|
+
}, null, 2));
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
function memoryRestore(args) {
|
|
856
|
+
return memoryBrief(args);
|
|
857
|
+
}
|
|
858
|
+
|
|
599
859
|
function memberGitIdentity(args) {
|
|
600
860
|
const state = readState();
|
|
601
861
|
if (!state.initialized) throw new Error('MOP is not initialized.');
|
|
@@ -604,12 +864,12 @@ function memberGitIdentity(args) {
|
|
|
604
864
|
if (!member) throw new Error('Unknown actor.');
|
|
605
865
|
const agent = requireActiveAgent(state, actor);
|
|
606
866
|
const name = String(args.name || member.displayName || actor);
|
|
607
|
-
const email =
|
|
867
|
+
const email = String(args.email || 'github-noreply');
|
|
608
868
|
const githubUsername = String(args['github-username'] || member.gitIdentity?.githubUsername || '');
|
|
609
|
-
member.gitIdentity =
|
|
869
|
+
member.gitIdentity = resolveGitIdentityInput(state, actor, name, email, githubUsername);
|
|
610
870
|
appendLedger(state, actor, 'git-identity', `Updated git identity for ${actor}.`, agent);
|
|
611
871
|
writeState(state);
|
|
612
|
-
console.log(`Git identity set for ${actor}: ${name} <${email}>${githubUsername ? ` github=${githubUsername}` : ''}`);
|
|
872
|
+
console.log(`Git identity set for ${actor}: ${member.gitIdentity.name} <${member.gitIdentity.email}>${member.gitIdentity.githubUsername ? ` github=${member.gitIdentity.githubUsername}` : ''}`);
|
|
613
873
|
}
|
|
614
874
|
|
|
615
875
|
function validate() {
|
|
@@ -621,8 +881,13 @@ function validate() {
|
|
|
621
881
|
if (!Array.isArray(state.agentCatalog)) errors.push('agentCatalog must be array');
|
|
622
882
|
if (state.activeAgents && typeof state.activeAgents !== 'object') errors.push('activeAgents must be object');
|
|
623
883
|
if (state.agentPolicy && typeof state.agentPolicy !== 'object') errors.push('agentPolicy must be object');
|
|
884
|
+
if (state.answerPolicy && typeof state.answerPolicy !== 'object') errors.push('answerPolicy must be object');
|
|
885
|
+
if (state.memoryPolicy && typeof state.memoryPolicy !== 'object') errors.push('memoryPolicy must be object');
|
|
624
886
|
if (state.agentRouter && typeof state.agentRouter !== 'object') errors.push('agentRouter must be object');
|
|
625
887
|
if (state.partyMode && typeof state.partyMode !== 'object') errors.push('partyMode must be object');
|
|
888
|
+
if (state.autosync?.githubIdentity && typeof state.autosync.githubIdentity !== 'object') {
|
|
889
|
+
errors.push('autosync.githubIdentity must be object');
|
|
890
|
+
}
|
|
626
891
|
if (state.projectRootPolicy && typeof state.projectRootPolicy !== 'object') errors.push('projectRootPolicy must be object');
|
|
627
892
|
if (state.projectRootPolicy?.rules && !Array.isArray(state.projectRootPolicy.rules)) {
|
|
628
893
|
errors.push('projectRootPolicy.rules must be array');
|
|
@@ -703,6 +968,8 @@ function status() {
|
|
|
703
968
|
return [codename, agent ? { name: agent.name, role: agent.role, id: agent.id } : { id: agentId, missing: true }];
|
|
704
969
|
})),
|
|
705
970
|
agentPolicy: state.agentPolicy || {},
|
|
971
|
+
answerPolicy: answerPolicy(state),
|
|
972
|
+
memoryPolicy: memoryPolicy(state),
|
|
706
973
|
agentRouter: state.agentRouter || {},
|
|
707
974
|
partyMode: state.partyMode || {},
|
|
708
975
|
workflow: {
|
|
@@ -756,19 +1023,25 @@ function main() {
|
|
|
756
1023
|
if (command === 'agent' && subcommand === 'require') return agentRequire(args);
|
|
757
1024
|
if (command === 'agent' && subcommand === 'route') return agentRoute(args);
|
|
758
1025
|
if (command === 'agent' && subcommand === 'list') return agentList();
|
|
1026
|
+
if (command === 'memory' && subcommand === 'add') return memoryAdd(args);
|
|
1027
|
+
if (command === 'memory' && subcommand === 'brief') return memoryBrief(args);
|
|
1028
|
+
if (command === 'memory' && subcommand === 'restore') return memoryRestore(args);
|
|
759
1029
|
|
|
760
1030
|
console.log(`Usage:
|
|
761
1031
|
node .MOP/scripts/mop-core.mjs status
|
|
762
1032
|
node .MOP/scripts/mop-core.mjs validate
|
|
763
|
-
node .MOP/scripts/mop-core.mjs setup --project-name NAME --name DISPLAY --codename CODE --password PASS --mode solo|team --conversation-language LANG --coding-language LANG --git-email EMAIL [--git-name NAME] [--github-username USER] [--github-url URL]
|
|
1033
|
+
node .MOP/scripts/mop-core.mjs setup --project-name NAME --name DISPLAY --codename CODE --password PASS --mode solo|team --conversation-language LANG --coding-language LANG [--git-email github-noreply|EMAIL] [--git-name NAME] [--github-username USER] [--github-url URL]
|
|
764
1034
|
node .MOP/scripts/mop-core.mjs login --codename CODE --password PASS
|
|
765
|
-
node .MOP/scripts/mop-core.mjs member git-identity --actor CODE --name NAME --email EMAIL [--github-username USER]
|
|
1035
|
+
node .MOP/scripts/mop-core.mjs member git-identity --actor CODE --name NAME [--email github-noreply|EMAIL] [--github-username USER]
|
|
766
1036
|
node .MOP/scripts/mop-core.mjs agent activate --actor CODE --role ROLE --title TITLE --name NAME
|
|
767
1037
|
node .MOP/scripts/mop-core.mjs agent use --actor CODE --name NAME
|
|
768
1038
|
node .MOP/scripts/mop-core.mjs agent current --actor CODE
|
|
769
1039
|
node .MOP/scripts/mop-core.mjs agent require --actor CODE [--role ROLE] [--title TITLE]
|
|
770
1040
|
node .MOP/scripts/mop-core.mjs agent route --actor CODE --task "task text"
|
|
771
|
-
node .MOP/scripts/mop-core.mjs agent list
|
|
1041
|
+
node .MOP/scripts/mop-core.mjs agent list
|
|
1042
|
+
node .MOP/scripts/mop-core.mjs memory brief --actor CODE [--month YYYY-MM]
|
|
1043
|
+
node .MOP/scripts/mop-core.mjs memory add --actor CODE --kind conversation --summary "what happened"
|
|
1044
|
+
node .MOP/scripts/mop-core.mjs memory restore --actor CODE`);
|
|
772
1045
|
}
|
|
773
1046
|
|
|
774
1047
|
try {
|
package/.agents/AGENTS.md
CHANGED
|
@@ -14,6 +14,13 @@ Before doing anything, read `.MOP/STATE.json` and follow
|
|
|
14
14
|
- After login, run the Agent Router: every conversation and action must route
|
|
15
15
|
to one primary named agent. Check with `mop-core.mjs agent route --actor
|
|
16
16
|
<codename> --task "<user task>"`.
|
|
17
|
+
- Before answering, restore monthly memory with
|
|
18
|
+
`mop-core.mjs memory brief --actor <codename>`.
|
|
19
|
+
- Every authenticated user-facing answer must start with the routed
|
|
20
|
+
`answerContract.firstLine`, for example:
|
|
21
|
+
`agent: <agent-name> (<agent-role>) to <user>`.
|
|
22
|
+
- After meaningful work, save memory with
|
|
23
|
+
`mop-core.mjs memory add --actor <codename> --kind conversation --summary "<outcome>"`.
|
|
17
24
|
- If the router says clarification is needed, ask the clarifying questions
|
|
18
25
|
before implementation.
|
|
19
26
|
- If the router activates Party Mode, show visible agent-to-agent dialogue using
|
|
@@ -27,6 +34,10 @@ Before doing anything, read `.MOP/STATE.json` and follow
|
|
|
27
34
|
- For risky changes, run adversarial review before implementation or merge.
|
|
28
35
|
- When a role agent first appears, ask the user to name it before using it as a
|
|
29
36
|
personal agent.
|
|
37
|
+
- Autosycn member commits must use the active member GitHub account. By default,
|
|
38
|
+
MOP derives `ID+USERNAME@users.noreply.github.com` from `gh api user` and
|
|
39
|
+
refuses mismatched GitHub accounts. `BURHAN-MOP` identity is reserved for
|
|
40
|
+
merge guardian commits only.
|
|
30
41
|
|
|
31
42
|
## Operating Rules
|
|
32
43
|
|
package/AGENTS.md
CHANGED
|
@@ -14,6 +14,13 @@ follow `.MOP/PROTOCOL.md`.
|
|
|
14
14
|
It selects one primary role and may recommend any number of support roles
|
|
15
15
|
when genuinely needed. If no named agent exists for a needed role, ask the
|
|
16
16
|
user to name it and save it with `agent activate`.
|
|
17
|
+
- Before answering an authenticated task, restore monthly memory:
|
|
18
|
+
`mop-core.mjs memory brief --actor <codename>`.
|
|
19
|
+
- Every authenticated user-facing answer must start with the routed named agent
|
|
20
|
+
line from `answerContract.firstLine`, for example:
|
|
21
|
+
`agent: <agent-name> (<agent-role>) to <user>`.
|
|
22
|
+
- After meaningful work or a useful answer, save memory:
|
|
23
|
+
`mop-core.mjs memory add --actor <codename> --kind conversation --summary "<one-line outcome>"`.
|
|
17
24
|
|
|
18
25
|
| Role | Name | Description |
|
|
19
26
|
| :--- | :--- | :--- |
|
|
@@ -39,8 +46,8 @@ follow `.MOP/PROTOCOL.md`.
|
|
|
39
46
|
|
|
40
47
|
For `/mop-setup`, ask in order: project name (default folder name), owner display
|
|
41
48
|
name, owner codename, password, solo/team, conversation language, coding
|
|
42
|
-
language, GitHub link, GitHub username, Git commit email
|
|
43
|
-
then whether to activate auto-deploy now or later.
|
|
49
|
+
language, GitHub link, GitHub username, Git commit email (`github-noreply` by
|
|
50
|
+
default), join mode if team, then whether to activate auto-deploy now or later.
|
|
44
51
|
|
|
45
52
|
Agent role/template names are not personal AI names. When a role agent is first
|
|
46
53
|
needed, ask the user to name it and save it with `mop-core.mjs agent activate`.
|
|
@@ -49,10 +56,15 @@ different agents.
|
|
|
49
56
|
The active named agent must be recorded on every memory/ledger action. Do not
|
|
50
57
|
activate irrelevant agents; route to one primary agent first, then add support
|
|
51
58
|
agents through Party Mode only when useful.
|
|
59
|
+
If the assistant cannot show the active agent line, it must stop and repair the
|
|
60
|
+
agent route instead of answering invisibly.
|
|
52
61
|
|
|
53
62
|
Default skill: `autosycn`. After meaningful changes, use
|
|
54
63
|
`.MOP/scripts/mop-autosycn.mjs`. It must commit and merge with the
|
|
55
64
|
real member Git identity from state, never with the AI tool identity.
|
|
65
|
+
Member commits must use the active member GitHub account; by default MOP derives
|
|
66
|
+
`ID+USERNAME@users.noreply.github.com` from `gh api user` and refuses mismatched
|
|
67
|
+
GitHub accounts. `BURHAN-MOP` identity is only for merge guardian commits.
|
|
56
68
|
In team mode, run `preflight --actor <codename>` before starting work; work goes
|
|
57
69
|
to `dev/<codename>`. Every small or large change is pushed there first, then
|
|
58
70
|
BURHAN-MOP reviews and merges to `main` only when checks pass.
|
package/CLAUDE.md
CHANGED
|
@@ -14,6 +14,13 @@ follow `.MOP/PROTOCOL.md`.
|
|
|
14
14
|
It selects one primary role and may recommend any number of support roles
|
|
15
15
|
when genuinely needed. If no named agent exists for a needed role, ask the
|
|
16
16
|
user to name it and save it with `agent activate`.
|
|
17
|
+
- Before answering an authenticated task, restore monthly memory:
|
|
18
|
+
`mop-core.mjs memory brief --actor <codename>`.
|
|
19
|
+
- Every authenticated user-facing answer must start with the routed named agent
|
|
20
|
+
line from `answerContract.firstLine`, for example:
|
|
21
|
+
`agent: <agent-name> (<agent-role>) to <user>`.
|
|
22
|
+
- After meaningful work or a useful answer, save memory:
|
|
23
|
+
`mop-core.mjs memory add --actor <codename> --kind conversation --summary "<one-line outcome>"`.
|
|
17
24
|
- If the router marks the task as ambiguous, the named primary agent must ask
|
|
18
25
|
clarifying questions before implementation.
|
|
19
26
|
- If the router returns `partyMode.active: true`, use Party Mode. Show
|
|
@@ -34,10 +41,15 @@ different agents.
|
|
|
34
41
|
The active named agent must be recorded on every memory/ledger action. Do not
|
|
35
42
|
activate irrelevant agents; route to one primary agent first, then add support
|
|
36
43
|
agents through Party Mode only when useful.
|
|
44
|
+
If the assistant cannot show the active agent line, it must stop and repair the
|
|
45
|
+
agent route instead of answering invisibly.
|
|
37
46
|
|
|
38
47
|
Default skill: `autosycn`. After meaningful changes, use
|
|
39
48
|
`.MOP/scripts/mop-autosycn.mjs`. It must commit and merge with the
|
|
40
49
|
real member Git identity from state, never with the AI tool identity.
|
|
50
|
+
Member commits must use the active member GitHub account; by default MOP derives
|
|
51
|
+
`ID+USERNAME@users.noreply.github.com` from `gh api user` and refuses mismatched
|
|
52
|
+
GitHub accounts. `BURHAN-MOP` identity is only for merge guardian commits.
|
|
41
53
|
In team mode, run `preflight --actor <codename>` before starting work; work goes
|
|
42
54
|
to `dev/<codename>`. Every small or large change is pushed there first, then
|
|
43
55
|
BURHAN-MOP reviews and merges to `main` only when checks pass.
|
package/GEMINI.md
CHANGED
|
@@ -9,6 +9,16 @@ rules are imported below.
|
|
|
9
9
|
|
|
10
10
|
- First action still applies: read `.MOP/STATE.json` and follow
|
|
11
11
|
`.MOP/PROTOCOL.md`.
|
|
12
|
+
- After authentication, run `mop-core.mjs memory brief --actor <codename>` and
|
|
13
|
+
`mop-core.mjs agent route --actor <codename> --task "<user task>"` before
|
|
14
|
+
answering.
|
|
15
|
+
- Every authenticated answer must start with `answerContract.firstLine`, for
|
|
16
|
+
example: `agent: <agent-name> (<agent-role>) to <user>`.
|
|
17
|
+
- Save a one-line memory after meaningful work with
|
|
18
|
+
`mop-core.mjs memory add --actor <codename> --kind conversation --summary "<outcome>"`.
|
|
19
|
+
- Autosycn member commits must use the active member GitHub account. MOP derives
|
|
20
|
+
`ID+USERNAME@users.noreply.github.com` from `gh api user` by default and
|
|
21
|
+
refuses mismatched GitHub accounts.
|
|
12
22
|
- Treat the current workspace root as the project root; do not create a nested
|
|
13
23
|
project wrapper folder for app work unless the user explicitly asks for it.
|
|
14
24
|
- Use `.gemini/settings.json` for context filenames and MCP server setup.
|
package/README.bm.md
CHANGED
|
@@ -142,7 +142,7 @@ dan merge hanya bila workflow selamat.
|
|
|
142
142
|
| --- | --- |
|
|
143
143
|
| npm package | [`burhan-mop`](https://www.npmjs.com/package/burhan-mop) |
|
|
144
144
|
| command latest | `npx burhan-mop install` |
|
|
145
|
-
| GitHub release | [`v0.1.
|
|
145
|
+
| GitHub release | [`v0.1.7`](https://github.com/BURHANDEV-ENTERPRISE/BURHAN-MOP/releases/tag/v0.1.7) |
|
|
146
146
|
| Node | `>=20` |
|
|
147
147
|
|
|
148
148
|
## Links
|
package/README.md
CHANGED
|
@@ -141,7 +141,7 @@ and merges only when the workflow is safe.
|
|
|
141
141
|
| --- | --- |
|
|
142
142
|
| npm package | [`burhan-mop`](https://www.npmjs.com/package/burhan-mop) |
|
|
143
143
|
| latest command | `npx burhan-mop install` |
|
|
144
|
-
| GitHub release | [`v0.1.
|
|
144
|
+
| GitHub release | [`v0.1.7`](https://github.com/BURHANDEV-ENTERPRISE/BURHAN-MOP/releases/tag/v0.1.7) |
|
|
145
145
|
| Node | `>=20` |
|
|
146
146
|
|
|
147
147
|
## Links
|