multi-agents-cli 1.1.38 → 1.1.39

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.
@@ -142,7 +142,8 @@ the agent must:
142
142
 
143
143
  1. Resolve the project root (OS-agnostic, works on Mac/Linux/Windows):
144
144
  ```bash
145
- git rev-parse --show-toplevel
145
+ git rev-parse --path-format=absolute --git-common-dir
146
+ # then take the parent directory of that path - this is PROJECT_ROOT
146
147
  ```
147
148
  Store this as PROJECT_ROOT. All subsequent paths (`.scaffold/`, `BUILD_STATE.md`, `CONTRACTS.md`, etc.) resolve from PROJECT_ROOT, never from the worktree directory.
148
149
  2. Read `BUILD_STATE.md` at PROJECT_ROOT - understand what has been built
@@ -269,7 +270,7 @@ You are operating inside a git worktree — NOT the main repo root.
269
270
  Before editing ANY file, verify you are in your own worktree:
270
271
 
271
272
  ```bash
272
- git rev-parse --show-toplevel # this is your root stay within it
273
+ git rev-parse --show-toplevel # this is your own worktree root - stay within it
273
274
  ```
274
275
 
275
276
  Never edit files outside your worktree path. If BUILD_STATE.md or
@@ -1406,7 +1406,7 @@ Mark each step complete. Only proceed to the task below when all are checked.
1406
1406
  fs.appendFileSync(beBuildStatePath, beLogEntry, 'utf8');
1407
1407
  try {
1408
1408
  execSync('git add BUILD_STATE.md', { cwd: ROOT, stdio: 'pipe' });
1409
- execSync(`git commit --no-verify -m "build: INIT task started on backend [${beBranchName}]"`, { cwd: ROOT, stdio: 'pipe' });
1409
+ execSync(`git commit --no-gpg-sign --no-verify -m "build: INIT task started on backend [${beBranchName}]"`, { cwd: ROOT, stdio: 'pipe' });
1410
1410
  console.log(` ${green('✓')} BUILD_STATE.md updated`);
1411
1411
  } catch {}
1412
1412
  }
@@ -1696,7 +1696,7 @@ ${excludedUrls}
1696
1696
 
1697
1697
  try {
1698
1698
  execSync('git add BUILD_STATE.md', { cwd: ROOT, stdio: 'pipe' });
1699
- execSync(`git commit --no-verify -m "build: ${agent} task started on ${project} [${branchName}]"`, { cwd: ROOT, stdio: 'pipe' });
1699
+ execSync(`git commit --no-gpg-sign --no-verify -m "build: ${agent} task started on ${project} [${branchName}]"`, { cwd: ROOT, stdio: 'pipe' });
1700
1700
  console.log(` ${green('✓')} BUILD_STATE.md committed to main`);
1701
1701
  } catch (err) {
1702
1702
  console.log(` ${yellow('!')} Could not commit BUILD_STATE.md - commit manually if needed`);
@@ -9,6 +9,7 @@
9
9
  const fs = require('fs');
10
10
  const path = require('path');
11
11
  const { execSync, spawn } = require('child_process');
12
+ const { sync } = require('./sync');
12
13
 
13
14
  const ROOT = path.join(__dirname, '..');
14
15
  const WORKFLOW_DIR = __dirname;
@@ -70,6 +71,8 @@ const findCliRoot = () => {
70
71
  };
71
72
 
72
73
  const main = async () => {
74
+ await sync({ mode: 'auto' });
75
+
73
76
  const stamped = getStampedVersion();
74
77
  const installed = getCurrentVersion();
75
78
 
@@ -0,0 +1,313 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * sync.js State reconciler.
6
+ *
7
+ * Ground truth is git. Reconciles .scaffold/.tracking.json and BUILD_STATE.md
8
+ * against actual worktrees, branches and merge status. Runs automatically from
9
+ * run.js before every agent launch, and standalone via npm run sync.
10
+ *
11
+ * Design note: status field is read agnostically. Any non null status that is
12
+ * not COMPLETED or PENDING counts as an in flight (active) slot, so the checks
13
+ * fire whether agent.js writes ACTIVE or IN PROGRESS.
14
+ */
15
+
16
+ const fs = require('fs');
17
+ const path = require('path');
18
+ const { execSync } = require('child_process');
19
+
20
+ const ROOT = path.join(__dirname, '..');
21
+ const TRACKING = path.join(ROOT, '.scaffold', '.tracking.json');
22
+ const BUILD = path.join(ROOT, 'BUILD_STATE.md');
23
+
24
+ // ANSI
25
+ const c = { reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m', green: '\x1b[32m', yellow: '\x1b[33m', red: '\x1b[31m' };
26
+ const dim = (s) => `${c.dim}${s}${c.reset}`;
27
+ const green = (s) => `${c.green}${s}${c.reset}`;
28
+ const yellow = (s) => `${c.yellow}${s}${c.reset}`;
29
+ const red = (s) => `${c.red}${s}${c.reset}`;
30
+ const bold = (s) => `${c.bold}${s}${c.reset}`;
31
+
32
+ const gitRead = (cmd) => {
33
+ try { return execSync(`git ${cmd}`, { cwd: ROOT, stdio: 'pipe', encoding: 'utf8' }).trim(); }
34
+ catch { return null; }
35
+ };
36
+
37
+ const getWorktrees = () => {
38
+ const out = gitRead('worktree list --porcelain');
39
+ if (!out) return [];
40
+ const wts = [];
41
+ for (const block of out.split('\n\n')) {
42
+ const lines = block.split('\n');
43
+ const p = lines.find(l => l.startsWith('worktree '));
44
+ const b = lines.find(l => l.startsWith('branch '));
45
+ if (p && b) {
46
+ wts.push({
47
+ path: p.replace('worktree ', '').trim(),
48
+ branch: b.replace('branch refs/heads/', '').trim(),
49
+ });
50
+ }
51
+ }
52
+ return wts;
53
+ };
54
+
55
+ const getLocalBranches = () => {
56
+ const out = gitRead('branch --format=%(refname:short)');
57
+ return out ? out.split('\n').map(s => s.trim()).filter(Boolean) : [];
58
+ };
59
+
60
+ const getMergedBranches = () => {
61
+ const out = gitRead('branch --merged main --format=%(refname:short)');
62
+ return out ? out.split('\n').map(s => s.trim()).filter(Boolean) : [];
63
+ };
64
+
65
+ const worktreeHealthy = (wtPath) => {
66
+ if (!wtPath || !fs.existsSync(wtPath)) return false;
67
+ return fs.existsSync(path.join(wtPath, '.git'));
68
+ };
69
+
70
+ const loadTracking = () => {
71
+ try { return JSON.parse(fs.readFileSync(TRACKING, 'utf8')); } catch { return null; }
72
+ };
73
+ const saveTracking = (t) => fs.writeFileSync(TRACKING, JSON.stringify(t, null, 2) + '\n', 'utf8');
74
+ const emptySlot = () => ({ branch: null, timestamp: null, launchedAt: null, status: null, missingCount: 0, worktreePath: null });
75
+
76
+ const eachSlot = (t, fn) => {
77
+ for (const scope of Object.keys(t)) {
78
+ for (const agent of Object.keys(t[scope])) {
79
+ fn(scope, agent, t[scope][agent]);
80
+ }
81
+ }
82
+ };
83
+
84
+ const TERMINAL = new Set(['COMPLETED']);
85
+ const PARKED = new Set(['PENDING']);
86
+ const isActive = (slot) => slot.status != null && !TERMINAL.has(slot.status) && !PARKED.has(slot.status);
87
+
88
+ const SECTION = { client: '## Client State', backend: '## Backend State', shared: '## Shared' };
89
+
90
+ const flipCheckbox = (content, scope, agent) => {
91
+ const header = SECTION[scope];
92
+ if (!header) return content;
93
+ const start = content.indexOf(header);
94
+ if (start === -1) return content;
95
+ const rest = content.slice(start + header.length);
96
+ const nextH = rest.indexOf('\n## ');
97
+ const blockEnd = nextH === -1 ? content.length : start + header.length + nextH;
98
+ const before = content.slice(0, start);
99
+ const block = content.slice(start, blockEnd);
100
+ const after = content.slice(blockEnd);
101
+ const patched = block.replace(`- [ ] ${agent} `, `- [x] ${agent} `);
102
+ return before + patched + after;
103
+ };
104
+
105
+ const ensureCompletedRow = (content, scope, agent, branch) => {
106
+ let patched = false, appended = false;
107
+ const esc = branch.replace(/\//g, '\\/');
108
+
109
+ const inProg = new RegExp(`(\\| [^|]+ \\| [^|]+ \\| [^|]+ \\| [^|]+ \\|) IN PROGRESS (\\| ${esc} \\|)`);
110
+ if (inProg.test(content)) {
111
+ content = content.replace(inProg, `$1 COMPLETED $2`);
112
+ patched = true;
113
+ } else if (!content.includes(branch)) {
114
+ const date = new Date().toISOString().split('T')[0];
115
+ const row = `| ${date} | ${agent} | ${scope} | reconciled by sync | COMPLETED | ${branch} |`;
116
+ const lines = content.split('\n');
117
+ let lastRow = -1;
118
+ for (let i = 0; i < lines.length; i++) if (/^\|/.test(lines[i])) lastRow = i;
119
+ if (lastRow !== -1) { lines.splice(lastRow + 1, 0, row); content = lines.join('\n'); appended = true; }
120
+ }
121
+ content = flipCheckbox(content, scope, agent);
122
+ return { content, patched, appended };
123
+ };
124
+
125
+ const findTaskMd = (wtPath, scope) => {
126
+ const candidates = [
127
+ path.join(wtPath, 'TASK.md'),
128
+ path.join(wtPath, scope, 'TASK.md'),
129
+ path.join(wtPath, 'client', 'TASK.md'),
130
+ path.join(wtPath, 'backend', 'TASK.md'),
131
+ ];
132
+ return candidates.find(p => fs.existsSync(p)) || null;
133
+ };
134
+
135
+ const writeSyncNotice = (taskPath, actions, stamp) => {
136
+ const block = [
137
+ ``,
138
+ `<!-- SYNC NOTICE ${stamp} -->`,
139
+ `## Sync notice`,
140
+ `sync.js reconciled shared state at this session start:`,
141
+ ...actions.map(a => `- ${a}`),
142
+ `Main BUILD_STATE.md and .tracking.json now reflect the above.`,
143
+ `Re read \`git show main:BUILD_STATE.md\` before continuing.`,
144
+ `<!-- END SYNC NOTICE -->`,
145
+ ``,
146
+ ].join('\n');
147
+ fs.appendFileSync(taskPath, block, 'utf8');
148
+ };
149
+
150
+ const arrowSelect = async (message, choices) => {
151
+ try {
152
+ const prompts = require('prompts');
153
+ const res = await prompts({
154
+ type: 'select', name: 'v', message,
155
+ choices: choices.map((c2, i) => ({ title: c2, value: i })),
156
+ });
157
+ return typeof res.v === 'number' ? res.v : 0;
158
+ } catch {
159
+ console.log(`\n ${message}`);
160
+ choices.forEach((c2, i) => console.log(` ${dim(`${i + 1}.`)} ${c2}`));
161
+ return 0;
162
+ }
163
+ };
164
+
165
+ async function sync(opts = {}) {
166
+ const mode = opts.mode || 'standalone';
167
+ const tracking = loadTracking();
168
+ if (!tracking) { if (mode === 'standalone') console.log(dim(' sync: no .tracking.json, nothing to reconcile')); return; }
169
+
170
+ const localBranches = getLocalBranches();
171
+ const mergedBranches = getMergedBranches();
172
+ const worktrees = getWorktrees();
173
+
174
+ const actions = [];
175
+ const decisions = [];
176
+ const orphans = [];
177
+ let changed = false;
178
+ let buildChanged = false;
179
+ const inFlightPatched = new Set();
180
+
181
+ // Check 1 branch existence
182
+ eachSlot(tracking, (scope, agent, slot) => {
183
+ if (!slot.branch) return;
184
+ if (!localBranches.includes(slot.branch) && isActive(slot)) {
185
+ slot.status = 'MISSING'; changed = true;
186
+ actions.push(`${scope}/${agent}: branch ${slot.branch} gone, marked MISSING`);
187
+ }
188
+ });
189
+
190
+ // Check 2 worktree health
191
+ eachSlot(tracking, (scope, agent, slot) => {
192
+ if (!isActive(slot) && slot.status !== 'MISSING') return;
193
+ const branchLive = localBranches.includes(slot.branch);
194
+ const wt = worktrees.find(w => w.branch === slot.branch);
195
+ const pathLive = wt ? worktreeHealthy(wt.path) : worktreeHealthy(slot.worktreePath);
196
+
197
+ if (!pathLive && branchLive && slot.status !== 'MISSING') {
198
+ slot.status = 'MISSING'; changed = true;
199
+ actions.push(`${scope}/${agent}: worktree missing, branch alive, marked MISSING`);
200
+ } else if (!pathLive && !branchLive) {
201
+ Object.assign(slot, emptySlot()); changed = true;
202
+ actions.push(`${scope}/${agent}: worktree and branch both gone, tracking cleaned (ABANDONED)`);
203
+ }
204
+ });
205
+
206
+ // Check 3 merge reflection
207
+ const buildTargets = [];
208
+ eachSlot(tracking, (scope, agent, slot) => {
209
+ if (!slot.branch) return;
210
+ if (!isActive(slot) && slot.status !== 'MISSING') return;
211
+ if (mergedBranches.includes(slot.branch)) {
212
+ const wt = worktrees.find(w => w.branch === slot.branch);
213
+ if (wt && worktreeHealthy(wt.path)) orphans.push(`${scope}/${agent} -> ${wt.path}`);
214
+ slot.status = 'COMPLETED';
215
+ if (!slot.completedAt) slot.completedAt = new Date().toISOString();
216
+ slot.worktreePath = null;
217
+ changed = true;
218
+ actions.push(`${scope}/${agent}: branch merged into main, marked COMPLETED`);
219
+ buildTargets.push({ scope, agent, branch: slot.branch });
220
+ inFlightPatched.add(scope);
221
+ }
222
+ });
223
+
224
+ // Check 4 BUILD_STATE vs tracking drift
225
+ eachSlot(tracking, (scope, agent, slot) => {
226
+ if (slot.status !== 'COMPLETED' || !slot.branch) return;
227
+ if (!buildTargets.find(b => b.branch === slot.branch)) buildTargets.push({ scope, agent, branch: slot.branch });
228
+ });
229
+
230
+ if (buildTargets.length && fs.existsSync(BUILD)) {
231
+ let content = fs.readFileSync(BUILD, 'utf8');
232
+ for (const t of buildTargets) {
233
+ const r = ensureCompletedRow(content, t.scope, t.agent, t.branch);
234
+ if (r.content !== content) {
235
+ content = r.content; buildChanged = true;
236
+ if (r.appended) actions.push(`BUILD_STATE: appended COMPLETED row for ${t.scope}/${t.agent}`);
237
+ else if (r.patched) actions.push(`BUILD_STATE: ${t.scope}/${t.agent} IN PROGRESS -> COMPLETED`);
238
+ }
239
+ }
240
+ if (buildChanged) fs.writeFileSync(BUILD, content, 'utf8');
241
+ }
242
+
243
+ if (changed) saveTracking(tracking);
244
+
245
+ // Check 5 absent scaffold (advisory)
246
+ for (const scope of Object.keys(tracking)) {
247
+ if (scope === 'shared') continue;
248
+ const scopeDir = path.join(ROOT, scope);
249
+ if (!fs.existsSync(scopeDir)) continue;
250
+ const slots = Object.values(tracking[scope]);
251
+ const anyCompleted = slots.some(s => s.status === 'COMPLETED');
252
+ const anyActive = slots.some(s => isActive(s));
253
+ if (!anyCompleted && !anyActive) {
254
+ decisions.push(`${scope}: scope folder exists but no agent started run npm run agent to begin`);
255
+ }
256
+ }
257
+
258
+ // Agent awareness TASK.md notices
259
+ if (actions.length && inFlightPatched.size) {
260
+ const stamp = new Date().toISOString();
261
+ eachSlot(tracking, (scope, agent, slot) => {
262
+ if (!isActive(slot) || !inFlightPatched.has(scope)) return;
263
+ const wt = worktrees.find(w => w.branch === slot.branch);
264
+ if (!wt || !worktreeHealthy(wt.path)) return;
265
+ const taskPath = findTaskMd(wt.path, scope);
266
+ if (!taskPath) { decisions.push(`${scope}/${agent}: live worktree but no TASK.md found for sync notice`); return; }
267
+ try { writeSyncNotice(taskPath, actions, stamp); actions.push(`TASK.md notice written for in flight ${scope}/${agent}`); }
268
+ catch (e) { decisions.push(`${scope}/${agent}: TASK.md notice write failed ${e.message}`); }
269
+ });
270
+ }
271
+
272
+ // Check 6 dirty state files -> auto commit
273
+ const dirty = gitRead('status --porcelain') || '';
274
+ const dirtyState = dirty.split('\n')
275
+ .map(l => l.slice(3).trim())
276
+ .filter(f => f === 'BUILD_STATE.md' || f.endsWith('.tracking.json'));
277
+ if (dirtyState.length) {
278
+ try {
279
+ execSync('git add BUILD_STATE.md .scaffold/.tracking.json', { cwd: ROOT, stdio: 'pipe' });
280
+ execSync('git commit --no-gpg-sign --no-verify -m "sync: reconcile state files"', { cwd: ROOT, stdio: 'pipe' });
281
+ actions.push(`committed reconciled state files (${dirtyState.join(', ')})`);
282
+ } catch (e) {
283
+ decisions.push(`state files dirty but auto commit failed ${e.message}`);
284
+ }
285
+ }
286
+
287
+ const clean = !actions.length && !decisions.length && !orphans.length;
288
+ if (clean) { if (mode === 'standalone') console.log(green(' \u2713 state in sync')); return; }
289
+
290
+ console.log(`\n${bold(' Sync report')}`);
291
+ if (actions.length) {
292
+ console.log(green(' auto patched:'));
293
+ actions.forEach(a => console.log(dim(' \u00b7 ') + a));
294
+ }
295
+ if (orphans.length) {
296
+ console.log(yellow(' orphaned worktrees (merged, safe to remove):'));
297
+ orphans.forEach(o => console.log(dim(' \u00b7 ') + o + dim(' git worktree remove <path>')));
298
+ }
299
+ if (decisions.length) {
300
+ console.log(yellow(' needs your attention:'));
301
+ decisions.forEach(d => console.log(dim(' \u00b7 ') + d));
302
+ if (mode === 'standalone') {
303
+ await arrowSelect('How to proceed?', ['Acknowledge and continue', 'Abort']);
304
+ }
305
+ }
306
+ console.log('');
307
+ }
308
+
309
+ module.exports = { sync };
310
+
311
+ if (require.main === module) {
312
+ sync({ mode: 'standalone' }).catch(err => { console.error(red(' sync error: ') + err.message); process.exit(1); });
313
+ }
package/init.js CHANGED
@@ -679,6 +679,7 @@ Before starting any task, verify:
679
679
  restart: 'node .workflow/restart.js',
680
680
  reset: 'node .workflow/reset.js',
681
681
  complete: 'node .workflow/complete.js',
682
+ sync: 'node .workflow/sync.js',
682
683
  },
683
684
  };
684
685
  fs.writeFileSync(path.join(ROOT, 'package.json'), JSON.stringify(userPackage, null, 2), 'utf8');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "multi-agents-cli",
3
- "version": "1.1.38",
3
+ "version": "1.1.39",
4
4
  "description": "Multi-agent workflow orchestration for Claude Code — isolated git worktrees, structured state tracking, autonomous task chaining",
5
5
  "keywords": [
6
6
  "claude-code",