@minionry/minion 0.7.36 → 0.7.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.
- package/bin/commands/minions.js +19 -0
- package/bin/minion.js +33 -4
- package/dist/server/cli/headless/claude-invoker-process.js +23 -7
- package/dist/server/cli/headless/claude-invoker-stream.js +8 -3
- package/dist/server/cli/headless/haiku-assessments.js +2 -1
- package/dist/server/cli/headless/mcp-config.js +2 -1
- package/dist/server/engines/claude/claude-command.js +1 -0
- package/dist/server/engines/factory.js +2 -0
- package/dist/server/engines/hermes/HermesEngine.js +112 -219
- package/dist/server/engines/hermes/hermes-gateway-registry.js +467 -0
- package/dist/server/index.js +10 -4
- package/dist/server/mcp/bouncer-integration.js +3 -2
- package/dist/server/mcp/classifier/ClaudeBouncerClassifier.js +35 -8
- package/dist/server/mcp/classifier/shadow-eval-scheduler.js +42 -0
- package/dist/server/mcp/classifier/telemetry.js +0 -0
- package/dist/server/mcp/permission-channels.js +176 -0
- package/dist/server/mcp/security-analysis.js +9 -1
- package/dist/server/mcp/security-audit.js +22 -2
- package/dist/server/mcp/security-patterns.js +73 -1
- package/dist/server/mcp/server.js +80 -50
- package/dist/server/routes/internal.js +6 -1
- package/dist/server/routes/notifications.js +6 -18
- package/dist/server/server-setup.js +24 -48
- package/dist/server/services/analytics.js +4 -2
- package/dist/server/services/browser/host-mock.js +6 -1
- package/dist/server/services/browser/host.js +27 -3
- package/dist/server/services/browser/reasoning-agent.js +3 -1
- package/dist/server/services/chain/chain-engine.js +494 -0
- package/dist/server/services/chain/chain-store.js +542 -0
- package/dist/server/services/git/haiku.js +2 -2
- package/dist/server/services/plan/agents/check-injection.md +43 -2
- package/dist/server/services/plan/agents/review-code.md +62 -3
- package/dist/server/services/plan/agents/review-quality.md +52 -2
- package/dist/server/services/plan/board-export.js +4 -0
- package/dist/server/services/plan/composer-prompt.js +27 -8
- package/dist/server/services/plan/composer.js +10 -4
- package/dist/server/services/plan/config-installer.js +4 -83
- package/dist/server/services/plan/executor.js +155 -73
- package/dist/server/services/plan/issue-effort.js +9 -21
- package/dist/server/services/plan/issue-prompt-builder.js +72 -9
- package/dist/server/services/plan/parser-core.js +6 -1
- package/dist/server/services/plan/quality-delta.js +6 -24
- package/dist/server/services/plan/readiness-planner.js +12 -4
- package/dist/server/services/plan/record-file.js +52 -0
- package/dist/server/services/plan/review-approval.js +64 -0
- package/dist/server/services/plan/review-gate.js +186 -77
- package/dist/server/services/plan/review-outcome.js +146 -0
- package/dist/server/services/plan/review-report.js +28 -1
- package/dist/server/services/plan/review-target.js +57 -0
- package/dist/server/services/plan/state-reconciler.js +3 -3
- package/dist/server/services/plan/template-diff.js +2 -0
- package/dist/server/services/plan/template-instantiator.js +4 -0
- package/dist/server/services/plan/watcher.js +11 -1
- package/dist/server/services/platform-reconnect.js +7 -0
- package/dist/server/services/platform-token-lifecycle.js +2 -1
- package/dist/server/services/platform.js +293 -50
- package/dist/server/services/relay-http-client.js +107 -0
- package/dist/server/services/schedule/agent-schedule-store.js +69 -8
- package/dist/server/services/schedule/agent-scheduler.js +48 -7
- package/dist/server/services/schedule/daily-timing.js +52 -0
- package/dist/server/services/schedule/prompt-file.js +55 -0
- package/dist/server/services/schedule/run-bootstrap.js +20 -5
- package/dist/server/services/schedule/schedule-store.js +54 -4
- package/dist/server/services/schedule/schedule-wire.js +17 -0
- package/dist/server/services/schedule/scheduler.js +43 -9
- package/dist/server/services/sdk/agent-schedule.js +294 -0
- package/dist/server/services/sdk/agents.js +95 -13
- package/dist/server/services/sdk/app-tools.js +44 -7
- package/dist/server/services/sdk/atomic-write.js +75 -0
- package/dist/server/services/sdk/browser.js +5 -1
- package/dist/server/services/sdk/composer.js +2 -0
- package/dist/server/services/sdk/file-change-notify.js +60 -13
- package/dist/server/services/sdk/files-app.js +12 -7
- package/dist/server/services/sdk/files-search.js +14 -0
- package/dist/server/services/sdk/files-transfer.js +1 -1
- package/dist/server/services/sdk/files-watch.js +721 -0
- package/dist/server/services/sdk/files-workspace.js +37 -11
- package/dist/server/services/sdk/inference-claude.js +4 -2
- package/dist/server/services/sdk/inference.js +16 -2
- package/dist/server/services/sdk/pm-chain.js +0 -0
- package/dist/server/services/sdk/pm-schedule.js +72 -15
- package/dist/server/services/sdk/pm.js +22 -0
- package/dist/server/services/sdk/rate-limits.js +32 -0
- package/dist/server/services/sdk/registry.js +40 -2
- package/dist/server/services/sdk/terminal.js +1 -1
- package/dist/server/services/sentry.js +5 -1
- package/dist/server/services/terminal/pty-manager.js +26 -11
- package/dist/server/services/terminal/pty-utils.js +12 -1
- package/dist/server/services/terminal/terminal-modes.js +87 -0
- package/dist/server/services/timeline/assembler.js +2 -2
- package/dist/server/services/timeline/index.js +1 -1
- package/dist/server/services/websocket/agent-schedule-handlers.js +62 -4
- package/dist/server/services/websocket/ask-user-question-bridge.js +2 -2
- package/dist/server/services/websocket/browser-agent-runtime.js +3 -1
- package/dist/server/services/websocket/browser-handlers.js +5 -2
- package/dist/server/services/websocket/browser-viewer-lifecycle.js +8 -0
- package/dist/server/services/websocket/file-explorer-handlers.js +17 -0
- package/dist/server/services/websocket/file-transfer-http.js +12 -6
- package/dist/server/services/websocket/handler.js +185 -33
- package/dist/server/services/websocket/msg-id-tracker.js +94 -19
- package/dist/server/services/websocket/plan-execution-handlers.js +33 -5
- package/dist/server/services/websocket/plan-handlers.js +10 -5
- package/dist/server/services/websocket/plan-helpers.js +23 -0
- package/dist/server/services/websocket/plan-issue-handlers.js +161 -4
- package/dist/server/services/websocket/plan-sprint-handlers.js +3 -2
- package/dist/server/services/websocket/schedule-handlers.js +8 -2
- package/dist/server/services/websocket/sdk-agent-host.js +5 -1
- package/dist/server/services/websocket/sdk-agent-schedule-host.js +30 -0
- package/dist/server/services/websocket/sdk-browser-host.js +11 -4
- package/dist/server/services/websocket/sdk-handlers.js +6 -1
- package/dist/server/services/websocket/sdk-pm-chain-host.js +20 -0
- package/dist/server/services/websocket/sdk-pm-execution-host.js +21 -14
- package/dist/server/services/websocket/sdk-pm-schedule-host.js +9 -0
- package/dist/server/services/websocket/sdk-terminal-host.js +7 -0
- package/dist/server/services/websocket/settings-handlers.js +6 -18
- package/dist/server/services/websocket/tab-broadcast.js +5 -1
- package/dist/server/services/websocket/terminal-handlers.js +51 -6
- package/dist/server/services/websocket/types.js +6 -5
- package/dist/server/services/websocket/viewer-interest.js +30 -0
- package/package.json +6 -6
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
// Copyright (c) 2025-present Minionry, Inc.
|
|
2
|
+
// SPDX-License-Identifier: LicenseRef-Minionry-Software
|
|
3
|
+
import { MAX_REVIEW_ATTEMPTS, } from './types.js';
|
|
4
|
+
const DEFAULT_CONFIDENCE = 'medium';
|
|
5
|
+
export const REVIEW_CONFIDENCES = ['high', 'medium', 'low'];
|
|
6
|
+
export const REVIEW_BLOCKER_KINDS = [
|
|
7
|
+
'needs_privileged_command',
|
|
8
|
+
'needs_credentials',
|
|
9
|
+
'missing_dependency',
|
|
10
|
+
'external_service',
|
|
11
|
+
'contradictory_criteria',
|
|
12
|
+
'out_of_scope_environment',
|
|
13
|
+
'no_progress',
|
|
14
|
+
];
|
|
15
|
+
const BLOCKER_SUMMARY = {
|
|
16
|
+
needs_privileged_command: 'needs a privileged (sudo/root) command no agent can run here',
|
|
17
|
+
needs_credentials: 'needs a credential or account this Space does not hold',
|
|
18
|
+
missing_dependency: 'needs a package, service or device that is not available here',
|
|
19
|
+
external_service: 'depends on an external service that is unavailable here',
|
|
20
|
+
contradictory_criteria: 'its acceptance criteria contradict each other or the code they name',
|
|
21
|
+
out_of_scope_environment: 'the work cannot be done by an agent in this environment',
|
|
22
|
+
no_progress: 'two review attempts failed identically — the retry loop is not converging',
|
|
23
|
+
};
|
|
24
|
+
export function parseReviewConfidence(value) {
|
|
25
|
+
const normalized = String(value ?? '').trim().toLowerCase();
|
|
26
|
+
return REVIEW_CONFIDENCES.includes(normalized)
|
|
27
|
+
? normalized
|
|
28
|
+
: DEFAULT_CONFIDENCE;
|
|
29
|
+
}
|
|
30
|
+
export function parseBlockerKind(value) {
|
|
31
|
+
const normalized = String(value ?? '').trim().toLowerCase();
|
|
32
|
+
return REVIEW_BLOCKER_KINDS.includes(normalized)
|
|
33
|
+
? normalized
|
|
34
|
+
: null;
|
|
35
|
+
}
|
|
36
|
+
export function parseReviewBlocker(value) {
|
|
37
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
38
|
+
return null;
|
|
39
|
+
const raw = value;
|
|
40
|
+
const kind = parseBlockerKind(raw.kind);
|
|
41
|
+
if (!kind)
|
|
42
|
+
return null;
|
|
43
|
+
const detail = String(raw.detail ?? '').trim();
|
|
44
|
+
if (!detail)
|
|
45
|
+
return null;
|
|
46
|
+
const humanAction = String(raw.humanAction ?? raw.human_action ?? '').trim();
|
|
47
|
+
return { kind, detail, ...(humanAction ? { humanAction } : {}) };
|
|
48
|
+
}
|
|
49
|
+
export function reviewDisposition(result) {
|
|
50
|
+
if (result.passed)
|
|
51
|
+
return 'pass';
|
|
52
|
+
if (result.disposition === 'blocked' && result.blocker)
|
|
53
|
+
return 'blocked';
|
|
54
|
+
return 'revise';
|
|
55
|
+
}
|
|
56
|
+
export function reviewConfidence(result) {
|
|
57
|
+
return result.confidence ?? DEFAULT_CONFIDENCE;
|
|
58
|
+
}
|
|
59
|
+
export function shouldEscalate(result, isIncomplete) {
|
|
60
|
+
if (isIncomplete)
|
|
61
|
+
return false;
|
|
62
|
+
if (reviewDisposition(result) === 'blocked')
|
|
63
|
+
return true;
|
|
64
|
+
return reviewConfidence(result) === 'low';
|
|
65
|
+
}
|
|
66
|
+
export function failureFingerprint(result) {
|
|
67
|
+
const failed = result.checks.filter((check) => !check.passed);
|
|
68
|
+
const entries = failed.map((check) => ({ name: check.name, reason: normalizeReason(check) }));
|
|
69
|
+
if (entries.every((entry) => entry.reason.length === 0))
|
|
70
|
+
return '';
|
|
71
|
+
return entries.map((entry) => `${entry.name}:${entry.reason}`).sort().join('|');
|
|
72
|
+
}
|
|
73
|
+
export function isRepeatFailure(previous, current) {
|
|
74
|
+
if (!previous || previous.passed || current.passed)
|
|
75
|
+
return false;
|
|
76
|
+
const before = failureFingerprint(previous);
|
|
77
|
+
return before.length > 0 && before === failureFingerprint(current);
|
|
78
|
+
}
|
|
79
|
+
export function decideReviewRecovery(result, previous, attemptsBefore) {
|
|
80
|
+
const attempts = attemptsBefore + 1;
|
|
81
|
+
if (result.passed)
|
|
82
|
+
return { action: 'complete', reason: 'Review passed', attempts };
|
|
83
|
+
const blocker = reviewDisposition(result) === 'blocked' ? result.blocker : undefined;
|
|
84
|
+
if (blocker) {
|
|
85
|
+
return {
|
|
86
|
+
action: 'abandon',
|
|
87
|
+
reason: `Cannot pass review — ${BLOCKER_SUMMARY[blocker.kind]}: ${blocker.detail}`,
|
|
88
|
+
blocker,
|
|
89
|
+
attempts,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
if (isRepeatFailure(previous, result)) {
|
|
93
|
+
const noProgress = {
|
|
94
|
+
kind: 'no_progress',
|
|
95
|
+
detail: `Review attempts ${attempts - 1} and ${attempts} failed the same checks (${failedNames(result).join(', ')}) for the same reasons.`,
|
|
96
|
+
humanAction: 'Re-scope the issue or fix it by hand — re-running it unchanged will fail the same way.',
|
|
97
|
+
};
|
|
98
|
+
return {
|
|
99
|
+
action: 'abandon',
|
|
100
|
+
reason: `Cannot pass review — ${BLOCKER_SUMMARY.no_progress}: ${failedNames(result).join(', ')} failed identically twice.`,
|
|
101
|
+
blocker: noProgress,
|
|
102
|
+
attempts,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
if (attempts >= MAX_REVIEW_ATTEMPTS) {
|
|
106
|
+
return {
|
|
107
|
+
action: 'abandon',
|
|
108
|
+
reason: `Review failed ${attempts} times — cancelled to unblock dependents; the issue likely needs restructuring`,
|
|
109
|
+
attempts,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
return { action: 'retry', reason: `Review failed: ${failedNames(result).join(', ')}`, attempts };
|
|
113
|
+
}
|
|
114
|
+
export function failedNames(result) {
|
|
115
|
+
return result.checks.filter((check) => !check.passed).map((check) => check.name);
|
|
116
|
+
}
|
|
117
|
+
const HEADING_RE = /^#{1,6}[ \t]+\S/;
|
|
118
|
+
const BLOCKED_HEADING_RE = /^#{1,6}[ \t]*blocked\b/i;
|
|
119
|
+
export function parseAgentBlockedReport(text) {
|
|
120
|
+
const body = blockedSectionBody(text);
|
|
121
|
+
if (!body)
|
|
122
|
+
return null;
|
|
123
|
+
const kind = parseBlockerKind(body.match(/^[ \t]*[-*]?[ \t]*kind[ \t]*:[ \t]*`?([a-z_]+)`?/im)?.[1]);
|
|
124
|
+
if (!kind || kind === 'no_progress')
|
|
125
|
+
return null;
|
|
126
|
+
const reason = (body.match(/^[ \t]*[-*]?[ \t]*reason[ \t]*:[ \t]*(.+)$/im)?.[1] ?? '').trim().replace(/`/g, '');
|
|
127
|
+
return reason ? { kind, reason: reason.slice(0, 600) } : null;
|
|
128
|
+
}
|
|
129
|
+
function blockedSectionBody(text) {
|
|
130
|
+
const lines = text.split('\n');
|
|
131
|
+
const start = lines.findIndex((line) => BLOCKED_HEADING_RE.test(line));
|
|
132
|
+
if (start === -1)
|
|
133
|
+
return null;
|
|
134
|
+
const rest = lines.slice(start + 1);
|
|
135
|
+
const end = rest.findIndex((line) => HEADING_RE.test(line));
|
|
136
|
+
return (end === -1 ? rest : rest.slice(0, end)).join('\n');
|
|
137
|
+
}
|
|
138
|
+
function normalizeReason(check) {
|
|
139
|
+
const source = (check.remediation ?? '').trim() || check.details;
|
|
140
|
+
return source
|
|
141
|
+
.toLowerCase()
|
|
142
|
+
.replace(/\d+/g, '#')
|
|
143
|
+
.replace(/[^a-z#]+/g, ' ')
|
|
144
|
+
.trim()
|
|
145
|
+
.slice(0, 400);
|
|
146
|
+
}
|
|
@@ -33,8 +33,19 @@ export function toReviewReport(result) {
|
|
|
33
33
|
reviewedAt: result.reviewedAt,
|
|
34
34
|
autoPass: result.autoPass === true,
|
|
35
35
|
incompleteReason: verdict === 'incomplete' ? (checks[0]?.details ?? '').trim() : '',
|
|
36
|
+
reviewedBy: describeRunner(result.reviewedBy),
|
|
37
|
+
escalatedFrom: result.escalatedFrom ? `escalated from ${result.escalatedFrom.model}` : '',
|
|
38
|
+
confidence: result.confidence ? `${result.confidence} confidence` : '',
|
|
39
|
+
blocked: result.disposition === 'blocked' && result.blocker
|
|
40
|
+
? `${result.blocker.kind}: ${result.blocker.detail}${result.blocker.humanAction ? ` To unblock: ${result.blocker.humanAction}` : ''}`
|
|
41
|
+
: '',
|
|
36
42
|
};
|
|
37
43
|
}
|
|
44
|
+
function describeRunner(runner) {
|
|
45
|
+
if (!runner)
|
|
46
|
+
return '';
|
|
47
|
+
return runner.effortLevel === 'auto' ? runner.model : `${runner.model} at ${runner.effortLevel} effort`;
|
|
48
|
+
}
|
|
38
49
|
const VERDICT_TITLES = {
|
|
39
50
|
passed: '✅ Review passed',
|
|
40
51
|
failed: '❌ Review failed',
|
|
@@ -47,6 +58,9 @@ export class MarkdownReviewReportRenderer {
|
|
|
47
58
|
lines.push(report.incompleteReason || 'The reviewer returned no verdict.', '', 'Nothing was marked done — the issue stays in review for a retry.', '');
|
|
48
59
|
return lines.join('\n');
|
|
49
60
|
}
|
|
61
|
+
if (report.blocked) {
|
|
62
|
+
lines.push(`**This issue cannot pass review as written** — ${report.blocked}`, '', 'It has been cancelled with that reason recorded rather than re-dispatched.', '');
|
|
63
|
+
}
|
|
50
64
|
for (const section of report.sections)
|
|
51
65
|
lines.push(...renderSection(section));
|
|
52
66
|
return lines.join('\n');
|
|
@@ -72,6 +86,8 @@ function toSection(check) {
|
|
|
72
86
|
label: reviewCheckLabel(check.name),
|
|
73
87
|
passed: check.passed,
|
|
74
88
|
details: (check.details ?? '').trim(),
|
|
89
|
+
evidence: (check.evidence ?? '').trim(),
|
|
90
|
+
remediation: (check.remediation ?? '').trim(),
|
|
75
91
|
};
|
|
76
92
|
}
|
|
77
93
|
function subtitle(report) {
|
|
@@ -85,6 +101,12 @@ function subtitle(report) {
|
|
|
85
101
|
parts.push(`**${failed} of ${report.totalCount} checks failed**${named}`);
|
|
86
102
|
}
|
|
87
103
|
parts.push(report.issueType === 'code' ? 'code review' : 'quality review');
|
|
104
|
+
if (report.reviewedBy)
|
|
105
|
+
parts.push(report.reviewedBy);
|
|
106
|
+
if (report.escalatedFrom)
|
|
107
|
+
parts.push(report.escalatedFrom);
|
|
108
|
+
if (report.confidence)
|
|
109
|
+
parts.push(report.confidence);
|
|
88
110
|
if (report.autoPass)
|
|
89
111
|
parts.push('_auto-passed on an infrastructure failure_');
|
|
90
112
|
const when = formatReviewedAt(report.reviewedAt);
|
|
@@ -95,7 +117,12 @@ function subtitle(report) {
|
|
|
95
117
|
function renderSection(section) {
|
|
96
118
|
const head = `#### ${section.passed ? '✅' : '❌'} ${section.label}`;
|
|
97
119
|
const body = section.details ? renderDetailBody(section.details) : ['_The reviewer gave no reason._'];
|
|
98
|
-
|
|
120
|
+
const cited = [];
|
|
121
|
+
if (section.evidence)
|
|
122
|
+
cited.push('', `*Evidence:* ${section.evidence}`);
|
|
123
|
+
if (section.remediation)
|
|
124
|
+
cited.push('', `*To fix:* ${section.remediation}`);
|
|
125
|
+
return [head, '', ...body, ...cited, ''];
|
|
99
126
|
}
|
|
100
127
|
export function renderDetailBody(details) {
|
|
101
128
|
const facets = splitEnumeratedFacets(details);
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// Copyright (c) 2025-present Minionry, Inc.
|
|
2
|
+
// SPDX-License-Identifier: LicenseRef-Minionry-Software
|
|
3
|
+
import { engineForModel, parseEngineId } from '../../engines/types.js';
|
|
4
|
+
export const REVIEW_GATE_ENGINE = 'claude-code';
|
|
5
|
+
export const REVIEW_GATE_MODEL = 'fable';
|
|
6
|
+
export const REVIEW_GATE_EFFORT_LEVEL = 'high';
|
|
7
|
+
export const REVIEW_ESCALATION_EFFORT_LEVEL = 'xhigh';
|
|
8
|
+
const CLAUDE_REVIEW_LADDER = ['haiku', 'sonnet', 'opus', 'fable'];
|
|
9
|
+
const REVIEW_EFFORT_BY_MODEL = {
|
|
10
|
+
haiku: 'auto',
|
|
11
|
+
sonnet: 'high',
|
|
12
|
+
opus: 'high',
|
|
13
|
+
fable: 'high',
|
|
14
|
+
};
|
|
15
|
+
export function resolveReviewTarget(issue) {
|
|
16
|
+
const named = normalizeModel(issue.reviewModel);
|
|
17
|
+
if (named)
|
|
18
|
+
return buildTarget(named, issue.reviewEffort, 'issue', issue.engine);
|
|
19
|
+
const executionModel = normalizeModel(issue.model);
|
|
20
|
+
if (executionModel)
|
|
21
|
+
return buildTarget(executionModel, issue.reviewEffort, 'execution-model', issue.engine);
|
|
22
|
+
return buildTarget(REVIEW_GATE_MODEL, issue.reviewEffort, 'floor', null);
|
|
23
|
+
}
|
|
24
|
+
export function escalatedReviewTarget(primary) {
|
|
25
|
+
const rung = CLAUDE_REVIEW_LADDER.indexOf(primary.model);
|
|
26
|
+
const next = rung === -1 ? -1 : rung + 1;
|
|
27
|
+
if (next <= 0 || next >= CLAUDE_REVIEW_LADDER.length)
|
|
28
|
+
return null;
|
|
29
|
+
const model = CLAUDE_REVIEW_LADDER[next];
|
|
30
|
+
return {
|
|
31
|
+
engine: engineForModel(model),
|
|
32
|
+
model,
|
|
33
|
+
effortLevel: REVIEW_ESCALATION_EFFORT_LEVEL,
|
|
34
|
+
source: 'escalation',
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
export function describeReviewTarget(target) {
|
|
38
|
+
return target.effortLevel === 'auto'
|
|
39
|
+
? `${target.model} at its default effort`
|
|
40
|
+
: `${target.model} at ${target.effortLevel} effort`;
|
|
41
|
+
}
|
|
42
|
+
export function toReviewRunner(target) {
|
|
43
|
+
return { engine: target.engine, model: target.model, effortLevel: target.effortLevel };
|
|
44
|
+
}
|
|
45
|
+
function normalizeModel(value) {
|
|
46
|
+
const trimmed = (value ?? '').trim();
|
|
47
|
+
if (!trimmed || trimmed.toLowerCase() === 'default')
|
|
48
|
+
return null;
|
|
49
|
+
return trimmed.toLowerCase();
|
|
50
|
+
}
|
|
51
|
+
function buildTarget(model, reviewEffort, source, issueEngine) {
|
|
52
|
+
const derived = engineForModel(model);
|
|
53
|
+
const declared = parseEngineId(issueEngine);
|
|
54
|
+
const engine = declared === derived ? declared : derived;
|
|
55
|
+
const effort = (reviewEffort ?? '').trim() || REVIEW_EFFORT_BY_MODEL[model] || REVIEW_GATE_EFFORT_LEVEL;
|
|
56
|
+
return { engine, model, effortLevel: effort, source };
|
|
57
|
+
}
|
|
@@ -13,7 +13,7 @@ function categorizeIssues(issues, issueByPath) {
|
|
|
13
13
|
for (const issue of issues) {
|
|
14
14
|
if (issue.type === 'epic')
|
|
15
15
|
continue;
|
|
16
|
-
if (issue.status === 'in_progress' || issue.status === 'in_review') {
|
|
16
|
+
if (issue.status === 'in_progress' || issue.status === 'in_review' || issue.status === 'awaiting_approval') {
|
|
17
17
|
inProgress.push(issue);
|
|
18
18
|
}
|
|
19
19
|
else if (issue.blockedBy.length > 0 && issue.status !== 'done' && issue.status !== 'cancelled') {
|
|
@@ -87,7 +87,7 @@ function deriveEpicStatus(epic, issueByPath) {
|
|
|
87
87
|
const allFinished = childStatuses.every(s => s === 'done' || s === 'cancelled');
|
|
88
88
|
if (allFinished)
|
|
89
89
|
return 'done';
|
|
90
|
-
const anyStarted = childStatuses.some(s => s === 'in_progress' || s === 'in_review');
|
|
90
|
+
const anyStarted = childStatuses.some(s => s === 'in_progress' || s === 'in_review' || s === 'awaiting_approval');
|
|
91
91
|
if (anyStarted && epic.status !== 'in_progress')
|
|
92
92
|
return 'in_progress';
|
|
93
93
|
return null;
|
|
@@ -118,7 +118,7 @@ function deriveSprintStatus(sprint, issueByPath) {
|
|
|
118
118
|
const allFinished = statuses.every(s => s === 'done' || s === 'cancelled');
|
|
119
119
|
if (allFinished)
|
|
120
120
|
return 'completed';
|
|
121
|
-
const anyStarted = statuses.some(s => s === 'in_progress' || s === 'in_review');
|
|
121
|
+
const anyStarted = statuses.some(s => s === 'in_progress' || s === 'in_review' || s === 'awaiting_approval');
|
|
122
122
|
if (anyStarted)
|
|
123
123
|
return 'active';
|
|
124
124
|
return null;
|
|
@@ -251,6 +251,10 @@ function renderIssueFrontMatter(issue, title, refs, created) {
|
|
|
251
251
|
lines.push(`effort_level: ${issue.effort_level}`);
|
|
252
252
|
if (issue.fast_mode != null)
|
|
253
253
|
lines.push(`fast_mode: ${issue.fast_mode}`);
|
|
254
|
+
if (issue.review_model)
|
|
255
|
+
lines.push(`review_model: ${issue.review_model}`);
|
|
256
|
+
if (issue.review_effort)
|
|
257
|
+
lines.push(`review_effort: ${issue.review_effort}`);
|
|
254
258
|
lines.push('---');
|
|
255
259
|
return lines.join('\n');
|
|
256
260
|
}
|
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
// Copyright (c) 2025-present Minionry, Inc.
|
|
2
2
|
// SPDX-License-Identifier: LicenseRef-Minionry-Software
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
3
4
|
import { watch } from 'node:fs';
|
|
4
5
|
import { parsePlanDirectory, resolvePmDir } from './parser.js';
|
|
6
|
+
const lastPlanStateDigest = new Map();
|
|
7
|
+
export function broadcastPlanStateIfChanged(ctx, workingDir, state) {
|
|
8
|
+
const digest = createHash('sha1').update(JSON.stringify(state)).digest('base64');
|
|
9
|
+
if (lastPlanStateDigest.get(workingDir) === digest)
|
|
10
|
+
return false;
|
|
11
|
+
lastPlanStateDigest.set(workingDir, digest);
|
|
12
|
+
ctx.broadcastToAll({ type: 'planStateUpdated', data: state });
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
5
15
|
export class PlanWatcher {
|
|
6
16
|
watcher = null;
|
|
7
17
|
debounceTimer = null;
|
|
@@ -56,7 +66,7 @@ export class PlanWatcher {
|
|
|
56
66
|
try {
|
|
57
67
|
const fullState = parsePlanDirectory(this.workingDir);
|
|
58
68
|
if (fullState) {
|
|
59
|
-
this.ctx.
|
|
69
|
+
broadcastPlanStateIfChanged(this.ctx, this.workingDir, fullState);
|
|
60
70
|
for (const listener of this.updateListeners) {
|
|
61
71
|
try {
|
|
62
72
|
listener();
|
|
@@ -5,6 +5,7 @@ export const RELAY_CLOSE = {
|
|
|
5
5
|
AUTH_EXPIRED: 4008,
|
|
6
6
|
ORCHESTRA_ELSEWHERE: 4013,
|
|
7
7
|
SUPERSEDED: 4015,
|
|
8
|
+
RELAY_HANDOFF: 4016,
|
|
8
9
|
SERVER_ERROR: 4500,
|
|
9
10
|
};
|
|
10
11
|
export function classifyClose(code, reason, opts) {
|
|
@@ -14,10 +15,13 @@ export function classifyClose(code, reason, opts) {
|
|
|
14
15
|
return 'protocol_too_old';
|
|
15
16
|
if (code === RELAY_CLOSE.SUPERSEDED)
|
|
16
17
|
return 'superseded';
|
|
18
|
+
if (code === RELAY_CLOSE.RELAY_HANDOFF)
|
|
19
|
+
return 'handoff';
|
|
17
20
|
if (code === RELAY_CLOSE.AUTH_FAILED || reason?.includes('Unauthorized'))
|
|
18
21
|
return 'auth_failed';
|
|
19
22
|
return 'reconnect';
|
|
20
23
|
}
|
|
24
|
+
export const HANDOFF_RECONNECT_MAX_MS = 1_000;
|
|
21
25
|
export class ReconnectPolicy {
|
|
22
26
|
attempts = 0;
|
|
23
27
|
pairedAt = null;
|
|
@@ -44,6 +48,9 @@ export class ReconnectPolicy {
|
|
|
44
48
|
const jitter = base * this.jitterRatio * (2 * this.random() - 1);
|
|
45
49
|
return Math.max(0, Math.round(base + jitter));
|
|
46
50
|
}
|
|
51
|
+
handoffDelayMs() {
|
|
52
|
+
return Math.round(this.random() * HANDOFF_RECONNECT_MAX_MS);
|
|
53
|
+
}
|
|
47
54
|
reset() {
|
|
48
55
|
this.attempts = 0;
|
|
49
56
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Copyright (c) 2025-present Minionry, Inc.
|
|
2
2
|
// SPDX-License-Identifier: LicenseRef-Minionry-Software
|
|
3
3
|
import { getCredentials, shouldRefreshToken, updateCredentials, } from './platform-credentials.js';
|
|
4
|
+
import { relayHttp } from './relay-http-client.js';
|
|
4
5
|
const PERIODIC_CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
5
6
|
export class TokenLifecycle {
|
|
6
7
|
deps;
|
|
@@ -48,7 +49,7 @@ export class TokenLifecycle {
|
|
|
48
49
|
if (!creds?.token)
|
|
49
50
|
return false;
|
|
50
51
|
try {
|
|
51
|
-
const response = await fetch(`${this.deps.platformUrl}/api/auth/device/verify`, {
|
|
52
|
+
const response = await (this.deps.http ?? relayHttp).fetch(`${this.deps.platformUrl}/api/auth/device/verify`, {
|
|
52
53
|
method: 'POST',
|
|
53
54
|
headers: { 'Authorization': `Bearer ${creds.token}` },
|
|
54
55
|
});
|