@monoes/monomindcli 2.11.8 → 2.11.10
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/README.md +1 -1
- package/bin/cli.js +17 -8
- package/dist/src/commands/crash-reporting.d.ts +3 -1
- package/dist/src/commands/crash-reporting.d.ts.map +1 -1
- package/dist/src/commands/crash-reporting.js +26 -10
- package/dist/src/commands/crash-reporting.js.map +1 -1
- package/dist/src/commands/doctor-env-checks.d.ts +9 -0
- package/dist/src/commands/doctor-env-checks.d.ts.map +1 -1
- package/dist/src/commands/doctor-env-checks.js +31 -0
- package/dist/src/commands/doctor-env-checks.js.map +1 -1
- package/dist/src/commands/doctor-monoes-checks.d.ts +12 -0
- package/dist/src/commands/doctor-monoes-checks.d.ts.map +1 -1
- package/dist/src/commands/doctor-monoes-checks.js +142 -1
- package/dist/src/commands/doctor-monoes-checks.js.map +1 -1
- package/dist/src/commands/doctor-project-checks.d.ts.map +1 -1
- package/dist/src/commands/doctor-project-checks.js +8 -3
- package/dist/src/commands/doctor-project-checks.js.map +1 -1
- package/dist/src/commands/doctor.d.ts.map +1 -1
- package/dist/src/commands/doctor.js +11 -3
- package/dist/src/commands/doctor.js.map +1 -1
- package/dist/src/commands/mcp.d.ts.map +1 -1
- package/dist/src/commands/mcp.js +16 -0
- package/dist/src/commands/mcp.js.map +1 -1
- package/dist/src/commands/org-observe.d.ts.map +1 -1
- package/dist/src/commands/org-observe.js +29 -4
- package/dist/src/commands/org-observe.js.map +1 -1
- package/dist/src/commands/org.d.ts.map +1 -1
- package/dist/src/commands/org.js +12 -3
- package/dist/src/commands/org.js.map +1 -1
- package/dist/src/init/executor.d.ts.map +1 -1
- package/dist/src/init/executor.js +17 -0
- package/dist/src/init/executor.js.map +1 -1
- package/dist/src/init/mcp-generator.d.ts +2 -7
- package/dist/src/init/mcp-generator.d.ts.map +1 -1
- package/dist/src/init/mcp-generator.js +15 -17
- package/dist/src/init/mcp-generator.js.map +1 -1
- package/dist/src/init/write-claude.d.ts.map +1 -1
- package/dist/src/init/write-claude.js +9 -0
- package/dist/src/init/write-claude.js.map +1 -1
- package/dist/src/init/write-runtime-config.d.ts +8 -0
- package/dist/src/init/write-runtime-config.d.ts.map +1 -1
- package/dist/src/init/write-runtime-config.js +69 -14
- package/dist/src/init/write-runtime-config.js.map +1 -1
- package/dist/src/mcp/monoes-mcp-entry.mjs +152 -0
- package/dist/src/mcp/monoes-proxy.d.ts +48 -0
- package/dist/src/mcp/monoes-proxy.d.ts.map +1 -0
- package/dist/src/mcp/monoes-proxy.js +240 -0
- package/dist/src/mcp/monoes-proxy.js.map +1 -0
- package/dist/src/memory/embedding-operations.d.ts.map +1 -1
- package/dist/src/memory/embedding-operations.js +5 -0
- package/dist/src/memory/embedding-operations.js.map +1 -1
- package/dist/src/orgrt/completion-gate.d.ts +58 -0
- package/dist/src/orgrt/completion-gate.d.ts.map +1 -0
- package/dist/src/orgrt/completion-gate.js +86 -0
- package/dist/src/orgrt/completion-gate.js.map +1 -0
- package/dist/src/orgrt/daemon.d.ts +15 -0
- package/dist/src/orgrt/daemon.d.ts.map +1 -1
- package/dist/src/orgrt/daemon.js +194 -12
- package/dist/src/orgrt/daemon.js.map +1 -1
- package/dist/src/orgrt/file-roots.d.ts +57 -0
- package/dist/src/orgrt/file-roots.d.ts.map +1 -0
- package/dist/src/orgrt/file-roots.js +100 -0
- package/dist/src/orgrt/file-roots.js.map +1 -0
- package/dist/src/orgrt/policy-git.d.ts.map +1 -1
- package/dist/src/orgrt/policy-git.js +122 -4
- package/dist/src/orgrt/policy-git.js.map +1 -1
- package/dist/src/orgrt/policy.d.ts +13 -1
- package/dist/src/orgrt/policy.d.ts.map +1 -1
- package/dist/src/orgrt/policy.js +60 -11
- package/dist/src/orgrt/policy.js.map +1 -1
- package/dist/src/orgrt/reporting.d.ts +56 -0
- package/dist/src/orgrt/reporting.d.ts.map +1 -1
- package/dist/src/orgrt/reporting.js +42 -3
- package/dist/src/orgrt/reporting.js.map +1 -1
- package/dist/src/orgrt/role-sandbox.d.ts.map +1 -1
- package/dist/src/orgrt/role-sandbox.js +19 -49
- package/dist/src/orgrt/role-sandbox.js.map +1 -1
- package/dist/src/orgrt/scheduler-integration.d.ts.map +1 -1
- package/dist/src/orgrt/scheduler-integration.js +4 -2
- package/dist/src/orgrt/scheduler-integration.js.map +1 -1
- package/dist/src/orgrt/session.d.ts +5 -2
- package/dist/src/orgrt/session.d.ts.map +1 -1
- package/dist/src/orgrt/session.js +51 -7
- package/dist/src/orgrt/session.js.map +1 -1
- package/dist/src/orgrt/task-dag.d.ts +11 -0
- package/dist/src/orgrt/task-dag.d.ts.map +1 -1
- package/dist/src/orgrt/task-dag.js +19 -0
- package/dist/src/orgrt/task-dag.js.map +1 -1
- package/dist/src/orgrt/types.d.ts +6 -0
- package/dist/src/orgrt/types.d.ts.map +1 -1
- package/dist/src/orgrt/types.js +10 -0
- package/dist/src/orgrt/types.js.map +1 -1
- package/dist/src/prompt.d.ts.map +1 -1
- package/dist/src/prompt.js +14 -0
- package/dist/src/prompt.js.map +1 -1
- package/dist/src/services/crash-consent.d.ts +35 -0
- package/dist/src/services/crash-consent.d.ts.map +1 -0
- package/dist/src/services/crash-consent.js +100 -0
- package/dist/src/services/crash-consent.js.map +1 -0
- package/dist/src/services/crash-lock.d.ts +22 -0
- package/dist/src/services/crash-lock.d.ts.map +1 -0
- package/dist/src/services/crash-lock.js +86 -0
- package/dist/src/services/crash-lock.js.map +1 -0
- package/dist/src/services/crash-race-timeout.d.ts +13 -0
- package/dist/src/services/crash-race-timeout.d.ts.map +1 -0
- package/dist/src/services/crash-race-timeout.js +15 -0
- package/dist/src/services/crash-race-timeout.js.map +1 -0
- package/dist/src/services/crash-reporter.d.ts +12 -4
- package/dist/src/services/crash-reporter.d.ts.map +1 -1
- package/dist/src/services/crash-reporter.js +105 -129
- package/dist/src/services/crash-reporter.js.map +1 -1
- package/dist/src/ui/dashboard.html +122 -0
- package/dist/src/ui/mastermind-diagram-fallback.html +14 -0
- package/dist/src/ui/orgs.html +12 -0
- package/dist/src/ui/routes-monoes.mjs +105 -21
- package/dist/src/utils/redaction.d.ts +17 -0
- package/dist/src/utils/redaction.d.ts.map +1 -1
- package/dist/src/utils/redaction.js +52 -7
- package/dist/src/utils/redaction.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +6 -6
package/dist/src/orgrt/daemon.js
CHANGED
|
@@ -14,12 +14,14 @@ import { OrgBus } from './bus.js';
|
|
|
14
14
|
import { captureCheckpoint, generateChecksum, isCheckpointExpired, migrateCheckpoint, restoreMailboxQueue, validateCheckpoint, } from './checkpoint.js';
|
|
15
15
|
import * as checkpointOps from './checkpoint-ops.js';
|
|
16
16
|
import { CodexAgentRunner } from './codex-runner.js';
|
|
17
|
+
import { checkCompletion } from './completion-gate.js';
|
|
17
18
|
import { CopilotAgentRunner } from './copilot-runner.js';
|
|
18
19
|
import * as crossOrg from './cross-org.js';
|
|
19
20
|
import { CrushAgentRunner } from './crush-runner.js';
|
|
20
21
|
import * as decisionOps from './decisions.js';
|
|
21
22
|
import { agentRoles, hasActiveEndpointWait, isEndpointRole, retryQueuedEndpoints, startEndpointRetryLoop, stopEndpointRetries, } from './endpoint-roles.js';
|
|
22
23
|
import { createFenceForRole, loadGlobalFenceConfig, mergeFenceConfigs, } from './fence.js';
|
|
24
|
+
import { fileToolRoots } from './file-roots.js';
|
|
23
25
|
import { attachForwarder } from './forwarder.js';
|
|
24
26
|
import { GrokAgentRunner } from './grok-runner.js';
|
|
25
27
|
import { HermesAgentRunner } from './hermes-runner.js';
|
|
@@ -205,6 +207,45 @@ export function activeRoleCount(org) {
|
|
|
205
207
|
n++;
|
|
206
208
|
return n;
|
|
207
209
|
}
|
|
210
|
+
/** #302: the `org_complete` consent gate's emit-and-return logic, extracted
|
|
211
|
+
* (matching `resolvedIdleNudgeCount`'s and `runOutcomeResult`'s precedent)
|
|
212
|
+
* so it is unit-testable without a live daemon, a running org, or the SDK's
|
|
213
|
+
* own tool-calling loop — none of which a test can drive directly, since
|
|
214
|
+
* `queryFn` replaces the whole SDK `query()`, tool orchestration included.
|
|
215
|
+
* `onComplete` gathers the facts (needs `this`/`running`) and calls this;
|
|
216
|
+
* this does the one decision (`checkCompletion`) and its two possible
|
|
217
|
+
* side effects. Returns the refusal string, or `null` to allow — exactly
|
|
218
|
+
* what the org_complete tool handler (session.ts) relays as the result. */
|
|
219
|
+
export function resolveOrgComplete(bus, role, outcome, summary, blocker, blockerDetail, runFacts) {
|
|
220
|
+
const refusal = checkCompletion({ outcome, blocker, blockerDetail, ...runFacts });
|
|
221
|
+
if (refusal) {
|
|
222
|
+
// Visible in `org logs` — a silent refusal reads to an operator as a
|
|
223
|
+
// hung boss, not a boss that was told no.
|
|
224
|
+
bus.emit({
|
|
225
|
+
type: 'audit',
|
|
226
|
+
from: role,
|
|
227
|
+
reason: 'org-complete-refused',
|
|
228
|
+
msg: refusal,
|
|
229
|
+
data: { outcome, blocker, blockerDetail },
|
|
230
|
+
});
|
|
231
|
+
return refusal;
|
|
232
|
+
}
|
|
233
|
+
// #302 AC6: the blocker must show up in RENDERED output, not just `data`
|
|
234
|
+
// — `org logs`'s formatter prints an event's `msg` verbatim and never
|
|
235
|
+
// looks at `data`, so a blocker recorded only there would satisfy a unit
|
|
236
|
+
// assertion and never reach a human reading the actual log.
|
|
237
|
+
const blockerSuffix = blocker
|
|
238
|
+
? ` (blocker: ${blocker}${blockerDetail ? ` — ${blockerDetail}` : ''})`
|
|
239
|
+
: '';
|
|
240
|
+
bus.emit({
|
|
241
|
+
type: 'status',
|
|
242
|
+
from: role,
|
|
243
|
+
reason: 'org-complete',
|
|
244
|
+
msg: `run outcome: ${outcome}${blockerSuffix}`,
|
|
245
|
+
data: { outcome, summary, blocker, blockerDetail },
|
|
246
|
+
});
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
208
249
|
export class OrgDaemon {
|
|
209
250
|
root;
|
|
210
251
|
opts;
|
|
@@ -448,7 +489,9 @@ export class OrgDaemon {
|
|
|
448
489
|
// Only this call can have registered the name (the reservation above
|
|
449
490
|
// holds until `finally`), so anything in the map is ours to tear down.
|
|
450
491
|
if (this.orgs.has(name)) {
|
|
451
|
-
|
|
492
|
+
// #302: tag the real cause so a run's history/report can never read
|
|
493
|
+
// this as a boss-attributed outcome — nothing here asked the boss.
|
|
494
|
+
await this.stopOrg(name, { closedBy: 'failed-start' }).catch((stopErr) => console.error(`org ${name}: teardown after failed start failed:`, stopErr instanceof Error ? stopErr.message : stopErr));
|
|
452
495
|
}
|
|
453
496
|
throw err;
|
|
454
497
|
}
|
|
@@ -460,6 +503,27 @@ export class OrgDaemon {
|
|
|
460
503
|
* reservation guard above runs synchronously, before any `await` in here —
|
|
461
504
|
* see the bug 2 comment in startOrg(). */
|
|
462
505
|
async startOrgInner(name, taskOverride, options) {
|
|
506
|
+
// #301: the PRIMARY fix — the stop-side backstops below (finishStop,
|
|
507
|
+
// process 'exit') can only run for a run that ends through code we
|
|
508
|
+
// control; a SIGKILL leaves .git/worktrees/<name> metadata behind with
|
|
509
|
+
// nothing left to clean it up, and so did every run that leaked before
|
|
510
|
+
// this fix existed. Pruning here turns "did the last run clean up,
|
|
511
|
+
// however it died?" into "a run always begins clean" — the only thing
|
|
512
|
+
// that recovers both the SIGKILL case and worktrees already orphaned
|
|
513
|
+
// before this daemon process started. Unconditional and best-effort for
|
|
514
|
+
// the same reasons as the stop-side prune (see finishStop): it only
|
|
515
|
+
// drops metadata whose worktree directory is already gone, so it cannot
|
|
516
|
+
// touch a live worktree, including the owner's.
|
|
517
|
+
try {
|
|
518
|
+
execFileSync('git', ['worktree', 'prune'], {
|
|
519
|
+
cwd: this.root,
|
|
520
|
+
stdio: 'ignore',
|
|
521
|
+
timeout: 30_000,
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
catch {
|
|
525
|
+
/* best-effort: not a git repo, git missing, or a wedged hook */
|
|
526
|
+
}
|
|
463
527
|
const defPath = join(this.root, ORG_DIR, `${name}.json`);
|
|
464
528
|
const def = OrgDefSchema.parse(JSON.parse(readFileSync(defPath, 'utf8')));
|
|
465
529
|
let run;
|
|
@@ -885,6 +949,26 @@ export class OrgDaemon {
|
|
|
885
949
|
catch {
|
|
886
950
|
/* best-effort */
|
|
887
951
|
}
|
|
952
|
+
// #301: `process.on('exit')` is the SECOND termination path — it fires
|
|
953
|
+
// for a normal stop too (finishStop's own prune above already covers
|
|
954
|
+
// that case, so this is a harmless idempotent repeat there) but also
|
|
955
|
+
// for every path that reaches it via an explicit `process.exit()`
|
|
956
|
+
// (org.ts's SIGTERM/SIGINT/SIGHUP handlers, uncaughtException,
|
|
957
|
+
// unhandledRejection) — none of which run finishStop's cleanup at all.
|
|
958
|
+
// execFileSync is already a static top-level import (see the comment
|
|
959
|
+
// above), so this stays synchronous-safe like the rest of this
|
|
960
|
+
// handler. SIGKILL cannot reach here — no in-process code runs for
|
|
961
|
+
// it — which is why prune-at-start exists as the complement.
|
|
962
|
+
try {
|
|
963
|
+
execFileSync('git', ['worktree', 'prune'], {
|
|
964
|
+
cwd: this.root,
|
|
965
|
+
stdio: 'ignore',
|
|
966
|
+
timeout: 30_000,
|
|
967
|
+
});
|
|
968
|
+
}
|
|
969
|
+
catch {
|
|
970
|
+
/* best-effort: not a git repo, git missing, or a wedged hook */
|
|
971
|
+
}
|
|
888
972
|
};
|
|
889
973
|
process.on('exit', crashCleanup);
|
|
890
974
|
running._crashCleanup = crashCleanup;
|
|
@@ -967,7 +1051,11 @@ export class OrgDaemon {
|
|
|
967
1051
|
const idleStop = (msg) => {
|
|
968
1052
|
stopping = true;
|
|
969
1053
|
bus.emit({ type: 'audit', reason: 'idle-stop', msg });
|
|
970
|
-
|
|
1054
|
+
// #302: closedBy: 'idle-stop' — the truth gate at finishStop's
|
|
1055
|
+
// history write reads this to record what actually happened
|
|
1056
|
+
// (including any runnable work left in org_tasks) instead of
|
|
1057
|
+
// letting a null outcome default to a plain "completed".
|
|
1058
|
+
this.stopOrg(name, { closedBy: 'idle-stop' }).catch((err) => console.error(`org ${name}: idle-stop failed:`, err instanceof Error ? err.message : err));
|
|
971
1059
|
};
|
|
972
1060
|
// #296: publish the projected stop time for `org status --json`. Only
|
|
973
1061
|
// written when it changes; a failed write must not throw out of the
|
|
@@ -1258,7 +1346,12 @@ export class OrgDaemon {
|
|
|
1258
1346
|
maxTokens: role.budget_tokens ?? perRoleBudget,
|
|
1259
1347
|
maxUsd: role.budget_usd,
|
|
1260
1348
|
...(role.policy ?? {}),
|
|
1261
|
-
}, bus, roleCwd
|
|
1349
|
+
}, bus, roleCwd,
|
|
1350
|
+
// #303: the file tools (Read/Write/Edit/Glob/Grep) get the same extra
|
|
1351
|
+
// roots the Bash sandbox already treats as writable (role-sandbox.ts) —
|
|
1352
|
+
// $TMPDIR, the org root, and any policy.sandbox.allowWrite entries.
|
|
1353
|
+
// $HOME is deliberately excluded; see file-roots.ts.
|
|
1354
|
+
fileToolRoots({ cwd: roleCwd, orgRoot: this.root }, role.policy?.sandbox));
|
|
1262
1355
|
policy.setToolContext({
|
|
1263
1356
|
providerPrefixes: () => roleProviderPrefixes(role),
|
|
1264
1357
|
trace: () => this.roleTrace(name, role.id),
|
|
@@ -1350,14 +1443,35 @@ export class OrgDaemon {
|
|
|
1350
1443
|
// true by actually denying tool use while this role has a pending gate,
|
|
1351
1444
|
// the same way pending approvals already do.
|
|
1352
1445
|
hasPendingGate: () => this.listGates(name, 'pending').some((g) => g.roleId === role.id),
|
|
1446
|
+
// #302: refuse an unsatisfiable org_complete BEFORE the 'org-complete'
|
|
1447
|
+
// status event ever exists — the bus subscriber at the top of this
|
|
1448
|
+
// function auto-stops the run on that event alone, so a refusal that
|
|
1449
|
+
// still emitted it would be undone by the very next tick regardless of
|
|
1450
|
+
// what this function returns to the tool handler.
|
|
1353
1451
|
onComplete: role.id === running.bossRoleId
|
|
1354
|
-
? (r, outcome, summary) => {
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1452
|
+
? (r, outcome, summary, blocker, blockerDetail) => {
|
|
1453
|
+
let maxBudgetFraction = 0;
|
|
1454
|
+
for (const rt of running.agents.values()) {
|
|
1455
|
+
const p = rt.policy;
|
|
1456
|
+
if (p.policy.maxTokens)
|
|
1457
|
+
maxBudgetFraction = Math.max(maxBudgetFraction, p.usage / p.policy.maxTokens);
|
|
1458
|
+
if (p.policy.maxUsd)
|
|
1459
|
+
maxBudgetFraction = Math.max(maxBudgetFraction, p.usageUsd / p.policy.maxUsd);
|
|
1460
|
+
}
|
|
1461
|
+
// Same predicates the idle watchdog uses for its own
|
|
1462
|
+
// legitimate-wait check (:1316-1328) — a pending gate or an
|
|
1463
|
+
// unanswered question is the same "genuinely waiting on a
|
|
1464
|
+
// human" fact either way.
|
|
1465
|
+
const pendingHumanWaits = this.listGates(name, 'pending').length +
|
|
1466
|
+
questionOps
|
|
1467
|
+
.readQuestions(this.root, name)
|
|
1468
|
+
.questions.filter((q) => q.answer === null).length;
|
|
1469
|
+
return resolveOrgComplete(bus, r, outcome, summary, blocker, blockerDetail, {
|
|
1470
|
+
mode: def.run_config.completion ?? 'boss',
|
|
1471
|
+
maxBudgetFraction,
|
|
1472
|
+
pendingHumanWaits,
|
|
1473
|
+
hasActiveBlock: running.taskDag?.hasActiveBlock(Date.now()) ?? false,
|
|
1474
|
+
hasPendingWork: running.taskDag?.hasPendingWork() ?? false,
|
|
1361
1475
|
});
|
|
1362
1476
|
}
|
|
1363
1477
|
: undefined,
|
|
@@ -1543,11 +1657,19 @@ export class OrgDaemon {
|
|
|
1543
1657
|
}
|
|
1544
1658
|
if (abortedByStop) {
|
|
1545
1659
|
runtime.status = 'ended';
|
|
1660
|
+
// #304: `message` here is always an abort string (see abortedByStop
|
|
1661
|
+
// above) — either "Operation aborted" or the SDK's "Claude Code
|
|
1662
|
+
// process aborted by user". Echoing it made a planned stop read as
|
|
1663
|
+
// a human interruption, and made roles of the same run read
|
|
1664
|
+
// differently. Report WHY the org stopped instead; the raw string
|
|
1665
|
+
// stays in `data` for debugging.
|
|
1666
|
+
const why = running.closedBy === 'org-complete' ? 'org_complete' : 'stop requested';
|
|
1546
1667
|
bus.emit({
|
|
1547
1668
|
type: 'status',
|
|
1548
1669
|
from: role.id,
|
|
1549
|
-
msg: `agent "${role.id}" stopped with the org (${
|
|
1670
|
+
msg: `agent "${role.id}" stopped with the org (${why})`,
|
|
1550
1671
|
reason: 'agent-stopped',
|
|
1672
|
+
data: { agentId: role.id, error: message },
|
|
1551
1673
|
});
|
|
1552
1674
|
return;
|
|
1553
1675
|
}
|
|
@@ -2000,6 +2122,10 @@ export class OrgDaemon {
|
|
|
2000
2122
|
return; // already stopped
|
|
2001
2123
|
org.pendingRoles?.clear(); // prevent lazy spawns after stop
|
|
2002
2124
|
this.spawning.delete(name); // clean up spawning tracking for this org
|
|
2125
|
+
// #304: set before the delete below, since that delete is what makes
|
|
2126
|
+
// abortedByStop (role loop) true — the role loop reads it off this same
|
|
2127
|
+
// object reference, not a fresh lookup (the org is gone from the map by then).
|
|
2128
|
+
org.closedBy = opts?.closedBy;
|
|
2003
2129
|
// Remove immediately (not at the end) so a concurrent stopOrg(name) call —
|
|
2004
2130
|
// e.g. stopAll() racing a scheduler-triggered stop on SIGINT — joins this
|
|
2005
2131
|
// shutdown via `stopping` instead of re-running the whole sequence and
|
|
@@ -2127,7 +2253,26 @@ export class OrgDaemon {
|
|
|
2127
2253
|
/* best-effort */
|
|
2128
2254
|
}
|
|
2129
2255
|
}
|
|
2130
|
-
|
|
2256
|
+
// #302 truth gate: every stop path funnels through here, so this is the
|
|
2257
|
+
// one place that can record how the run ACTUALLY ended, regardless of
|
|
2258
|
+
// which of the five paths triggered it. `closedBy` is undefined only for
|
|
2259
|
+
// a bare manual `stopOrg(name)` (CLI `org stop`, shutdown) — every
|
|
2260
|
+
// automated path above now tags its own real cause. reporting.ts reads
|
|
2261
|
+
// this event (reason: 'org-stopped') to decide whether the run's outcome
|
|
2262
|
+
// may be rendered as a boss-attributed 'partial'/'achieved' at all: only
|
|
2263
|
+
// closedBy === 'org-complete' may be.
|
|
2264
|
+
const runnableTasks = org.taskDag?.pendingTaskCount() ?? 0;
|
|
2265
|
+
// Rendered, not just recorded (#302 AC6, same reasoning as the
|
|
2266
|
+
// blockerSuffix above): `org logs` prints `msg` verbatim.
|
|
2267
|
+
const stopSuffix = closedBy && closedBy !== 'org-complete'
|
|
2268
|
+
? ` (${closedBy}${runnableTasks > 0 ? `, ${runnableTasks} task(s) left` : ''})`
|
|
2269
|
+
: '';
|
|
2270
|
+
org.bus.emit({
|
|
2271
|
+
type: 'status',
|
|
2272
|
+
reason: 'org-stopped',
|
|
2273
|
+
msg: `org stopped${stopSuffix}`,
|
|
2274
|
+
data: { closedBy, runnableTasks },
|
|
2275
|
+
});
|
|
2131
2276
|
await org.bus.flush();
|
|
2132
2277
|
// Append this run's summary to <org>/history.jsonl — read back from the
|
|
2133
2278
|
// flushed bus.jsonl (the full durable record) rather than the bounded
|
|
@@ -2230,6 +2375,43 @@ export class OrgDaemon {
|
|
|
2230
2375
|
}
|
|
2231
2376
|
}
|
|
2232
2377
|
}
|
|
2378
|
+
// #301: roles create their own linked worktrees with Bash (paths the
|
|
2379
|
+
// daemon never recorded — org.worktreePath/agent.worktreePath above are
|
|
2380
|
+
// only ever set for workspace: 'worktree'/'worktree-per-role', empty
|
|
2381
|
+
// for the common workspace: 'repo' shape), and deleting the working
|
|
2382
|
+
// directory from inside a role sandbox leaves .git/worktrees/<name>
|
|
2383
|
+
// behind — `git worktree list` then hides it, and it never gets
|
|
2384
|
+
// cleaned up. Unconditional on purpose: gating this on
|
|
2385
|
+
// org/agent.worktreePath would skip exactly the runs that hit the bug.
|
|
2386
|
+
// prune only drops metadata whose worktree directory is already gone,
|
|
2387
|
+
// so a live worktree — including the owner's — is never touched; it is
|
|
2388
|
+
// idempotent; and the two removals just above already run `git
|
|
2389
|
+
// worktree remove --force` against this same repo from this same cwd,
|
|
2390
|
+
// so this is strictly less invasive than what already ships. Run after
|
|
2391
|
+
// both removal loops so a worktree just removed is also pruned.
|
|
2392
|
+
//
|
|
2393
|
+
// Bounded race, measured rather than assumed (same treatment as the
|
|
2394
|
+
// SIGKILL case above): an entry whose `gitdir` file is absent is
|
|
2395
|
+
// pruned unconditionally, and `--expire` cannot protect it — measured
|
|
2396
|
+
// across every window from `--expire=now` to `--expire=3.months.ago`,
|
|
2397
|
+
// a fresh no-gitdir entry is removed regardless, while `--expire` also
|
|
2398
|
+
// makes an already-deleted worktree SURVIVE, breaking the "a run
|
|
2399
|
+
// always begins clean" guarantee this fix exists to provide. So a
|
|
2400
|
+
// concurrent `git worktree add` by another process in this repo is
|
|
2401
|
+
// vulnerable for the microseconds between its `mkdir` and its
|
|
2402
|
+
// `gitdir` write. A mid-creation state cannot persist longer than
|
|
2403
|
+
// that, so an entry found in that state is dead metadata, not a live
|
|
2404
|
+
// worktree in progress.
|
|
2405
|
+
try {
|
|
2406
|
+
execFileSync('git', ['worktree', 'prune'], {
|
|
2407
|
+
cwd: this.root,
|
|
2408
|
+
stdio: 'ignore',
|
|
2409
|
+
timeout: 30_000,
|
|
2410
|
+
});
|
|
2411
|
+
}
|
|
2412
|
+
catch {
|
|
2413
|
+
/* best-effort: not a git repo, git missing, or a wedged hook */
|
|
2414
|
+
}
|
|
2233
2415
|
}
|
|
2234
2416
|
catch {
|
|
2235
2417
|
/* node:child_process unavailable — skip */
|