@geekbeer/minion 3.51.1 → 3.52.0
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/core/api.js +26 -2
- package/core/lib/board-task-poller.js +12 -1
- package/core/lib/dag-cron-poller.js +14 -1
- package/core/lib/dag-step-poller.js +17 -1
- package/core/lib/frozen-state.js +64 -0
- package/core/lib/revision-watcher.js +5 -1
- package/core/lib/step-poller.js +5 -1
- package/core/lib/thread-watcher.js +5 -1
- package/core/routes/admin.js +49 -0
- package/docs/api-reference.md +31 -3
- package/docs/task-guides.md +1 -1
- package/linux/server.js +2 -0
- package/package.json +1 -1
- package/rules/core.md +9 -1
- package/win/server.js +2 -0
package/core/api.js
CHANGED
|
@@ -6,6 +6,16 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
const { config, isHqConfigured } = require('./config')
|
|
9
|
+
const frozenState = require('./lib/frozen-state')
|
|
10
|
+
|
|
11
|
+
class BillingFrozenError extends Error {
|
|
12
|
+
constructor(reason) {
|
|
13
|
+
super(`Minion is billing-frozen: ${reason || 'unknown'}`)
|
|
14
|
+
this.name = 'BillingFrozenError'
|
|
15
|
+
this.statusCode = 402
|
|
16
|
+
this.billingFrozen = true
|
|
17
|
+
}
|
|
18
|
+
}
|
|
9
19
|
|
|
10
20
|
/**
|
|
11
21
|
* Send HTTP request to the HQ server
|
|
@@ -17,6 +27,10 @@ async function request(endpoint, options = {}) {
|
|
|
17
27
|
return { skipped: true, reason: 'HQ not configured' }
|
|
18
28
|
}
|
|
19
29
|
|
|
30
|
+
if (frozenState.isFrozen()) {
|
|
31
|
+
throw new BillingFrozenError(frozenState.getState().reason)
|
|
32
|
+
}
|
|
33
|
+
|
|
20
34
|
const url = `${config.HQ_URL}/api/minion${endpoint}`
|
|
21
35
|
|
|
22
36
|
const response = await fetch(url, {
|
|
@@ -28,10 +42,19 @@ async function request(endpoint, options = {}) {
|
|
|
28
42
|
},
|
|
29
43
|
})
|
|
30
44
|
|
|
31
|
-
|
|
45
|
+
let data = null
|
|
46
|
+
try {
|
|
47
|
+
data = await response.json()
|
|
48
|
+
} catch {
|
|
49
|
+
data = {}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (frozenState.maybeFreezeFromResponse(response, data)) {
|
|
53
|
+
throw new BillingFrozenError(data && data.reason)
|
|
54
|
+
}
|
|
32
55
|
|
|
33
56
|
if (!response.ok) {
|
|
34
|
-
const err = new Error(data.error || `API request failed: ${response.status}`)
|
|
57
|
+
const err = new Error((data && data.error) || `API request failed: ${response.status}`)
|
|
35
58
|
err.statusCode = response.status
|
|
36
59
|
throw err
|
|
37
60
|
}
|
|
@@ -203,4 +226,5 @@ module.exports = {
|
|
|
203
226
|
deleteThread,
|
|
204
227
|
createProjectMemory,
|
|
205
228
|
searchProjectMemories,
|
|
229
|
+
BillingFrozenError,
|
|
206
230
|
}
|
|
@@ -21,6 +21,8 @@
|
|
|
21
21
|
|
|
22
22
|
const { config, isHqConfigured } = require('../config')
|
|
23
23
|
const concurrency = require('./concurrency-manager')
|
|
24
|
+
const frozenState = require('./frozen-state')
|
|
25
|
+
const api = require('../api')
|
|
24
26
|
|
|
25
27
|
// Polling interval: 30 seconds (matches dag-step-poller).
|
|
26
28
|
const POLL_INTERVAL_MS = 30_000
|
|
@@ -45,6 +47,9 @@ async function hqRequest(endpoint, options = {}) {
|
|
|
45
47
|
if (!isHqConfigured()) {
|
|
46
48
|
return { skipped: true, reason: 'HQ not configured' }
|
|
47
49
|
}
|
|
50
|
+
if (frozenState.isFrozen()) {
|
|
51
|
+
throw new api.BillingFrozenError(frozenState.getState().reason)
|
|
52
|
+
}
|
|
48
53
|
const url = `${config.HQ_URL}${endpoint}`
|
|
49
54
|
const resp = await fetch(url, {
|
|
50
55
|
...options,
|
|
@@ -61,6 +66,9 @@ async function hqRequest(endpoint, options = {}) {
|
|
|
61
66
|
} catch {
|
|
62
67
|
data = { raw: text }
|
|
63
68
|
}
|
|
69
|
+
if (frozenState.maybeFreezeFromResponse(resp, data)) {
|
|
70
|
+
throw new api.BillingFrozenError(data && data.reason)
|
|
71
|
+
}
|
|
64
72
|
if (!resp.ok) {
|
|
65
73
|
const err = new Error(data.error || `HQ ${endpoint} failed: ${resp.status}`)
|
|
66
74
|
err.statusCode = resp.status
|
|
@@ -72,6 +80,7 @@ async function hqRequest(endpoint, options = {}) {
|
|
|
72
80
|
|
|
73
81
|
async function pollOnce() {
|
|
74
82
|
if (!isHqConfigured()) return
|
|
83
|
+
if (frozenState.isFrozen()) return
|
|
75
84
|
if (!runner) {
|
|
76
85
|
console.warn('[BoardTaskPoller] No runner injected, skipping poll')
|
|
77
86
|
return
|
|
@@ -117,7 +126,9 @@ async function pollOnce() {
|
|
|
117
126
|
})
|
|
118
127
|
}
|
|
119
128
|
} catch (err) {
|
|
120
|
-
if (err.
|
|
129
|
+
if (err.billingFrozen) {
|
|
130
|
+
console.log('[BoardTaskPoller] Billing frozen, suspending poll')
|
|
131
|
+
} else if (err.message?.includes('fetch failed') || err.message?.includes('ECONNREFUSED')) {
|
|
121
132
|
console.log('[BoardTaskPoller] HQ unreachable, will retry next cycle')
|
|
122
133
|
} else {
|
|
123
134
|
console.error(`[BoardTaskPoller] Poll error: ${err.message}`)
|
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
const { config, isHqConfigured } = require('../config')
|
|
15
|
+
const frozenState = require('./frozen-state')
|
|
16
|
+
const api = require('../api')
|
|
15
17
|
|
|
16
18
|
const POLL_INTERVAL_MS = 60_000
|
|
17
19
|
|
|
@@ -22,6 +24,7 @@ let lastFiredCount = 0
|
|
|
22
24
|
|
|
23
25
|
async function pollOnce() {
|
|
24
26
|
if (!isHqConfigured()) return
|
|
27
|
+
if (frozenState.isFrozen()) return
|
|
25
28
|
if (polling) return
|
|
26
29
|
|
|
27
30
|
polling = true
|
|
@@ -35,6 +38,14 @@ async function pollOnce() {
|
|
|
35
38
|
},
|
|
36
39
|
})
|
|
37
40
|
|
|
41
|
+
let payload = null
|
|
42
|
+
if (resp.status === 402) {
|
|
43
|
+
try { payload = await resp.json() } catch { payload = {} }
|
|
44
|
+
if (frozenState.maybeFreezeFromResponse(resp, payload)) {
|
|
45
|
+
throw new api.BillingFrozenError(payload && payload.reason)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
38
49
|
if (!resp.ok) {
|
|
39
50
|
throw new Error(`dag-cron-tick failed: ${resp.status}`)
|
|
40
51
|
}
|
|
@@ -58,7 +69,9 @@ async function pollOnce() {
|
|
|
58
69
|
}
|
|
59
70
|
}
|
|
60
71
|
} catch (err) {
|
|
61
|
-
if (err.
|
|
72
|
+
if (err.billingFrozen) {
|
|
73
|
+
console.log('[DagCronPoller] Billing frozen, suspending poll')
|
|
74
|
+
} else if (err.message?.includes('fetch failed') || err.message?.includes('ECONNREFUSED')) {
|
|
62
75
|
console.log('[DagCronPoller] HQ unreachable, will retry next cycle')
|
|
63
76
|
} else {
|
|
64
77
|
console.error(`[DagCronPoller] Poll error: ${err.message}`)
|
|
@@ -13,6 +13,7 @@ const { config, isHqConfigured } = require('../config')
|
|
|
13
13
|
const api = require('../api')
|
|
14
14
|
const variableStore = require('../stores/variable-store')
|
|
15
15
|
const concurrency = require('./concurrency-manager')
|
|
16
|
+
const frozenState = require('./frozen-state')
|
|
16
17
|
|
|
17
18
|
// Polling interval: 30 seconds (matches step-poller)
|
|
18
19
|
const POLL_INTERVAL_MS = 30_000
|
|
@@ -38,6 +39,10 @@ async function dagRequest(endpoint, options = {}) {
|
|
|
38
39
|
return { skipped: true, reason: 'HQ not configured' }
|
|
39
40
|
}
|
|
40
41
|
|
|
42
|
+
if (frozenState.isFrozen()) {
|
|
43
|
+
throw new api.BillingFrozenError(frozenState.getState().reason)
|
|
44
|
+
}
|
|
45
|
+
|
|
41
46
|
const url = `${config.HQ_URL}/api/dag/minion${endpoint}`
|
|
42
47
|
const resp = await fetch(url, {
|
|
43
48
|
...options,
|
|
@@ -48,6 +53,14 @@ async function dagRequest(endpoint, options = {}) {
|
|
|
48
53
|
},
|
|
49
54
|
})
|
|
50
55
|
|
|
56
|
+
let payload = null
|
|
57
|
+
if (resp.status === 402) {
|
|
58
|
+
try { payload = await resp.json() } catch { payload = {} }
|
|
59
|
+
if (frozenState.maybeFreezeFromResponse(resp, payload)) {
|
|
60
|
+
throw new api.BillingFrozenError(payload && payload.reason)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
51
64
|
if (!resp.ok) {
|
|
52
65
|
const err = new Error(`DAG API ${endpoint} failed: ${resp.status}`)
|
|
53
66
|
err.statusCode = resp.status
|
|
@@ -62,6 +75,7 @@ async function dagRequest(endpoint, options = {}) {
|
|
|
62
75
|
*/
|
|
63
76
|
async function pollOnce() {
|
|
64
77
|
if (!isHqConfigured()) return
|
|
78
|
+
if (frozenState.isFrozen()) return
|
|
65
79
|
if (polling) return
|
|
66
80
|
|
|
67
81
|
polling = true
|
|
@@ -90,7 +104,9 @@ async function pollOnce() {
|
|
|
90
104
|
promise
|
|
91
105
|
}
|
|
92
106
|
} catch (err) {
|
|
93
|
-
if (err.
|
|
107
|
+
if (err.billingFrozen) {
|
|
108
|
+
console.log('[DagPoller] Billing frozen, suspending poll')
|
|
109
|
+
} else if (err.message?.includes('fetch failed') || err.message?.includes('ECONNREFUSED')) {
|
|
94
110
|
console.log('[DagPoller] HQ unreachable, will retry next cycle')
|
|
95
111
|
} else {
|
|
96
112
|
console.error(`[DagPoller] Poll error: ${err.message}`)
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Frozen State Module
|
|
3
|
+
*
|
|
4
|
+
* Tracks billing-driven freeze state in-memory. When frozen, all pollers
|
|
5
|
+
* skip their work and the shared HTTP wrappers refuse outbound calls.
|
|
6
|
+
*
|
|
7
|
+
* State is intentionally NOT persisted to disk:
|
|
8
|
+
* - Recovery is driven by HQ pushing `restart-agent` after payment success.
|
|
9
|
+
* - Process restart naturally clears the in-memory flag.
|
|
10
|
+
* - On restart, if billing is still past_due, the next request will receive
|
|
11
|
+
* 402 from HQ and self-freeze again. Self-healing.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
let frozen = false
|
|
15
|
+
let frozenAt = null
|
|
16
|
+
let reason = null
|
|
17
|
+
|
|
18
|
+
function isFrozen() {
|
|
19
|
+
return frozen
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function setFrozen(opts = {}) {
|
|
23
|
+
if (frozen) return
|
|
24
|
+
frozen = true
|
|
25
|
+
frozenAt = new Date().toISOString()
|
|
26
|
+
reason = opts.reason || 'unknown'
|
|
27
|
+
console.log(`[FrozenState] Minion frozen: reason=${reason} at=${frozenAt}`)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function clearFrozen() {
|
|
31
|
+
if (!frozen) return
|
|
32
|
+
frozen = false
|
|
33
|
+
frozenAt = null
|
|
34
|
+
reason = null
|
|
35
|
+
console.log('[FrozenState] Minion unfrozen (in-memory state cleared)')
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function getState() {
|
|
39
|
+
return { frozen, frozenAt, reason }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Inspect a fetch Response for the 402 billing-frozen signal and
|
|
44
|
+
* self-freeze if matched. Returns true if frozen was set.
|
|
45
|
+
*
|
|
46
|
+
* Expected payload: `{ "error": "billing_frozen", "reason": "past_due", ... }`
|
|
47
|
+
*/
|
|
48
|
+
function maybeFreezeFromResponse(response, payload) {
|
|
49
|
+
if (response && response.status === 402) {
|
|
50
|
+
if (payload && payload.error === 'billing_frozen') {
|
|
51
|
+
setFrozen({ reason: payload.reason || 'billing_frozen' })
|
|
52
|
+
return true
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return false
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
module.exports = {
|
|
59
|
+
isFrozen,
|
|
60
|
+
setFrozen,
|
|
61
|
+
clearFrozen,
|
|
62
|
+
getState,
|
|
63
|
+
maybeFreezeFromResponse,
|
|
64
|
+
}
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
const { config, isHqConfigured } = require('../config')
|
|
17
17
|
const api = require('../api')
|
|
18
|
+
const frozenState = require('./frozen-state')
|
|
18
19
|
|
|
19
20
|
// Poll every 30 seconds (same frequency as step-poller)
|
|
20
21
|
const POLL_INTERVAL_MS = 30_000
|
|
@@ -33,6 +34,7 @@ const processingRevisions = new Set()
|
|
|
33
34
|
*/
|
|
34
35
|
async function pollOnce() {
|
|
35
36
|
if (!isHqConfigured()) return
|
|
37
|
+
if (frozenState.isFrozen()) return
|
|
36
38
|
if (polling) return
|
|
37
39
|
|
|
38
40
|
polling = true
|
|
@@ -59,7 +61,9 @@ async function pollOnce() {
|
|
|
59
61
|
}
|
|
60
62
|
}
|
|
61
63
|
} catch (err) {
|
|
62
|
-
if (err.
|
|
64
|
+
if (err.billingFrozen) {
|
|
65
|
+
console.log(`[RevisionWatcher] Billing frozen, suspending poll`)
|
|
66
|
+
} else if (err.message?.includes('fetch failed') || err.message?.includes('ECONNREFUSED')) {
|
|
63
67
|
console.log(`[RevisionWatcher] HQ unreachable, will retry next cycle`)
|
|
64
68
|
} else {
|
|
65
69
|
console.error(`[RevisionWatcher] Poll error: ${err.message}`)
|
package/core/lib/step-poller.js
CHANGED
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
const { config, isHqConfigured } = require('../config')
|
|
22
22
|
const api = require('../api')
|
|
23
23
|
const variableStore = require('../stores/variable-store')
|
|
24
|
+
const frozenState = require('./frozen-state')
|
|
24
25
|
|
|
25
26
|
// Polling interval: 30 seconds (matches heartbeat frequency)
|
|
26
27
|
const POLL_INTERVAL_MS = 30_000
|
|
@@ -42,6 +43,7 @@ let lastPollAt = null
|
|
|
42
43
|
*/
|
|
43
44
|
async function pollOnce() {
|
|
44
45
|
if (!isHqConfigured()) return
|
|
46
|
+
if (frozenState.isFrozen()) return
|
|
45
47
|
if (polling) return
|
|
46
48
|
|
|
47
49
|
polling = true
|
|
@@ -70,7 +72,9 @@ async function pollOnce() {
|
|
|
70
72
|
}
|
|
71
73
|
} catch (err) {
|
|
72
74
|
// Don't log network errors at error level — they're expected when HQ is temporarily unreachable
|
|
73
|
-
if (err.
|
|
75
|
+
if (err.billingFrozen) {
|
|
76
|
+
console.log(`[StepPoller] Billing frozen, suspending poll`)
|
|
77
|
+
} else if (err.message?.includes('fetch failed') || err.message?.includes('ECONNREFUSED')) {
|
|
74
78
|
console.log(`[StepPoller] HQ unreachable, will retry next cycle`)
|
|
75
79
|
} else {
|
|
76
80
|
console.error(`[StepPoller] Poll error: ${err.message}`)
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
|
|
21
21
|
const { config, isHqConfigured, isLlmConfigured } = require('../config')
|
|
22
22
|
const api = require('../api')
|
|
23
|
+
const frozenState = require('./frozen-state')
|
|
23
24
|
|
|
24
25
|
// Poll every 15 seconds
|
|
25
26
|
const POLL_INTERVAL_MS = 15_000
|
|
@@ -94,6 +95,7 @@ function isMentioned(thread, messages, myRole) {
|
|
|
94
95
|
*/
|
|
95
96
|
async function pollOnce() {
|
|
96
97
|
if (!isHqConfigured()) return
|
|
98
|
+
if (frozenState.isFrozen()) return
|
|
97
99
|
if (polling) return
|
|
98
100
|
|
|
99
101
|
polling = true
|
|
@@ -115,7 +117,9 @@ async function pollOnce() {
|
|
|
115
117
|
}
|
|
116
118
|
}
|
|
117
119
|
} catch (err) {
|
|
118
|
-
if (err.
|
|
120
|
+
if (err.billingFrozen) {
|
|
121
|
+
// Billing frozen — silent
|
|
122
|
+
} else if (err.message?.includes('fetch failed') || err.message?.includes('ECONNREFUSED')) {
|
|
119
123
|
// HQ unreachable — silent retry
|
|
120
124
|
} else {
|
|
121
125
|
console.error(`[ThreadWatcher] Poll error: ${err.message}`)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Admin routes
|
|
3
|
+
*
|
|
4
|
+
* HQ-only operations pushed via Cloudflare Tunnel. Currently exposes the
|
|
5
|
+
* billing-driven freeze endpoint. Recovery is intentionally NOT exposed
|
|
6
|
+
* here — HQ recovers a frozen minion by sending the existing
|
|
7
|
+
* `restart-agent` command (commands.js), which terminates the process and
|
|
8
|
+
* lets the in-memory frozen flag clear naturally.
|
|
9
|
+
*
|
|
10
|
+
* Endpoints:
|
|
11
|
+
* POST /api/admin/freeze - Set in-memory frozen state
|
|
12
|
+
* GET /api/admin/state - Diagnostic: report frozen state
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const { verifyToken } = require('../lib/auth')
|
|
16
|
+
const frozenState = require('../lib/frozen-state')
|
|
17
|
+
|
|
18
|
+
async function adminRoutes(fastify) {
|
|
19
|
+
fastify.post('/api/admin/freeze', async (request, reply) => {
|
|
20
|
+
if (!verifyToken(request)) {
|
|
21
|
+
reply.code(401)
|
|
22
|
+
return { success: false, error: 'Unauthorized' }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const body = request.body || {}
|
|
26
|
+
const reason = typeof body.reason === 'string' ? body.reason : 'past_due'
|
|
27
|
+
|
|
28
|
+
frozenState.setFrozen({ reason })
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
success: true,
|
|
32
|
+
state: frozenState.getState(),
|
|
33
|
+
}
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
fastify.get('/api/admin/state', async (request, reply) => {
|
|
37
|
+
if (!verifyToken(request)) {
|
|
38
|
+
reply.code(401)
|
|
39
|
+
return { success: false, error: 'Unauthorized' }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
success: true,
|
|
44
|
+
state: frozenState.getState(),
|
|
45
|
+
}
|
|
46
|
+
})
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
module.exports = { adminRoutes }
|
package/docs/api-reference.md
CHANGED
|
@@ -705,6 +705,23 @@ Note: 既読メールは受信後90日で自動削除される。未読メール
|
|
|
705
705
|
|
|
706
706
|
Available commands: `restart-agent`, `update-agent`, `restart-display`, `restart-all`, `status-services`
|
|
707
707
|
|
|
708
|
+
### Admin (HQ → Minion only)
|
|
709
|
+
|
|
710
|
+
HQ が課金状態の変化に応じてミニオンに直接プッシュするエンドポイント。
|
|
711
|
+
|
|
712
|
+
| Method | Endpoint | Description |
|
|
713
|
+
|--------|----------|-------------|
|
|
714
|
+
| POST | `/api/admin/freeze` | ポーラーを停止しHQへの発信を抑止する。Body: `{reason}` |
|
|
715
|
+
| GET | `/api/admin/state` | 現在のfrozen状態を取得 |
|
|
716
|
+
|
|
717
|
+
**Freeze の挙動 (v3.52.0〜):**
|
|
718
|
+
- ミニオン内のグローバル `frozen=true` (in-memory only) を立てる。
|
|
719
|
+
- 各ポーラー (step, dag-step, board-task, thread-watcher, dag-cron, revision-watcher, heartbeat) が `pollOnce()` 直前で `isFrozen()` を確認しスキップ。
|
|
720
|
+
- 共通HTTPクライアント (`core/api.js`, 各pollerの `*Request`) は HQ から `402 billing_frozen` を受信した時点で自動的に `frozen=true` をセット (HQからのpushが届かなくても自己防衛)。
|
|
721
|
+
- 状態は**ディスク永続化されない**。プロセス再起動で自動的にクリアされ、課金復活後の `restart-agent` コマンドで自然に解除される。
|
|
722
|
+
|
|
723
|
+
復旧は HQ が既存の `POST /api/command { command: "restart-agent" }` をプッシュすることで行う (専用 unfreeze エンドポイントは無い)。
|
|
724
|
+
|
|
708
725
|
---
|
|
709
726
|
|
|
710
727
|
## HQ API Endpoints (https://<HQ_URL>)
|
|
@@ -1337,7 +1354,7 @@ POST `/api/minion/dag-workflows/:id/publish` (body なし):
|
|
|
1337
1354
|
|------|---------------|------|
|
|
1338
1355
|
| `start` / `end` | なし | |
|
|
1339
1356
|
| `skill` | `skill_id`, `skill_version_id`, `assigned_role` | |
|
|
1340
|
-
| `review` | `review: {
|
|
1357
|
+
| `review` | `assigned_role`, `review: { criteria, revision_target }` | `assigned_role: 'human'` でダッシュボードからの人間レビュー、`'pm'`/`'engineer'`/`'accountant'` で将来の minion レビュー枠(現在は未実装、`waiting` で停止)。`revision` / `approved` エッジは自動生成 |
|
|
1341
1358
|
| `fan_out` | `fan_out_source`, `template` | `template` は sub-graph `{ nodes, edges }` |
|
|
1342
1359
|
| `join` | `join_mode`, `aggregation` | |
|
|
1343
1360
|
| `conditional` | `condition_type`, `branches` or `default_branch` | |
|
|
@@ -1350,13 +1367,24 @@ POST `/api/minion/dag-workflows/:id/publish` (body なし):
|
|
|
1350
1367
|
"label": "Quality Check",
|
|
1351
1368
|
"after": "skill_1",
|
|
1352
1369
|
"before": "end",
|
|
1370
|
+
"assigned_role": "human",
|
|
1353
1371
|
"review": {
|
|
1354
|
-
"reviewer_type": "human",
|
|
1355
1372
|
"criteria": "成果物の品質を確認",
|
|
1356
|
-
"revision_target": "skill_1"
|
|
1373
|
+
"revision_target": "skill_1",
|
|
1374
|
+
"preferred_reviewer_id": "<user UUID(任意・指定なしの場合は全プロジェクトメンバーに通知)>"
|
|
1357
1375
|
}
|
|
1358
1376
|
}
|
|
1359
1377
|
```
|
|
1378
|
+
|
|
1379
|
+
`assigned_role` の値:
|
|
1380
|
+
- `human` — ダッシュボードから人間が承認 (`workflow.review_requested` 通知が送信される)
|
|
1381
|
+
- `pm` / `engineer` / `accountant` — 将来の minion レビュー枠 (現在は未実装、`waiting` で停止)
|
|
1382
|
+
|
|
1383
|
+
`preferred_reviewer_id` (任意):
|
|
1384
|
+
- `assigned_role === 'human'` の場合: `profiles.user_id` を指定すると通知をその人に絞る。脱退済みなら全メンバーにフォールバック
|
|
1385
|
+
- `assigned_role !== 'human'` の場合: `minions.id` を指定(現在は未実装)
|
|
1386
|
+
|
|
1387
|
+
> 互換性: 旧形式 `review: { reviewer_type, reviewer_id }` も読み取り可能ですが、新規作成時は `assigned_role` + `preferred_reviewer_id` を使ってください。
|
|
1360
1388
|
→ `skill_1 → end` のエッジが削除され、以下が自動生成:
|
|
1361
1389
|
- `skill_1 → review_1` (normal)
|
|
1362
1390
|
- `review_1 → end` (approved)
|
package/docs/task-guides.md
CHANGED
package/linux/server.js
CHANGED
|
@@ -74,6 +74,7 @@ const { skillRoutes } = require('../core/routes/skills')
|
|
|
74
74
|
const { workflowRoutes } = require('../core/routes/workflows')
|
|
75
75
|
const { routineRoutes } = require('../core/routes/routines')
|
|
76
76
|
const { authRoutes } = require('../core/routes/auth')
|
|
77
|
+
const { adminRoutes } = require('../core/routes/admin')
|
|
77
78
|
const { variableRoutes } = require('../core/routes/variables')
|
|
78
79
|
const { memoryRoutes } = require('../core/routes/memory')
|
|
79
80
|
const { dailyLogRoutes } = require('../core/routes/daily-logs')
|
|
@@ -286,6 +287,7 @@ async function registerAllRoutes(app) {
|
|
|
286
287
|
await app.register(workflowRoutes, { workflowRunner })
|
|
287
288
|
await app.register(routineRoutes, { routineRunner })
|
|
288
289
|
await app.register(authRoutes)
|
|
290
|
+
await app.register(adminRoutes)
|
|
289
291
|
await app.register(variableRoutes)
|
|
290
292
|
await app.register(memoryRoutes)
|
|
291
293
|
await app.register(dailyLogRoutes)
|
package/package.json
CHANGED
package/rules/core.md
CHANGED
|
@@ -103,7 +103,15 @@ minion-cli --version # バージョン確認
|
|
|
103
103
|
|
|
104
104
|
`http://localhost:8080` — 認証: `Authorization: Bearer $API_TOKEN`
|
|
105
105
|
|
|
106
|
-
主なカテゴリ: Health, Skills, Workflows, Executions, Terminal, Files, Commands, Permissions
|
|
106
|
+
主なカテゴリ: Health, Skills, Workflows, Executions, Terminal, Files, Commands, Permissions, Admin (HQ-pushed freeze)
|
|
107
|
+
|
|
108
|
+
#### Billing-Driven Freeze (v3.52.0〜)
|
|
109
|
+
|
|
110
|
+
ユーザーの決済が `past_due` に陥ると、HQ から `POST /api/admin/freeze` がプッシュされ、ミニオン全ポーラーが停止する。
|
|
111
|
+
- 状態は in-memory のみ (frozen.json などのディスクファイルは存在しない)。
|
|
112
|
+
- HQ への発信中に `402 billing_frozen` を受けた場合も自動的にfrozen化される (HQ pushが届かなくても自己防衛)。
|
|
113
|
+
- 復旧は HQ が `restart-agent` コマンドをプッシュ → プロセス再起動 → in-memory フラグが消えて自動再開。
|
|
114
|
+
- API仕様は `~/.minion/docs/api-reference.md` の「Admin」セクション参照。
|
|
107
115
|
|
|
108
116
|
#### Permission Management
|
|
109
117
|
|
package/win/server.js
CHANGED
|
@@ -57,6 +57,7 @@ const { skillRoutes } = require('../core/routes/skills')
|
|
|
57
57
|
const { workflowRoutes } = require('../core/routes/workflows')
|
|
58
58
|
const { routineRoutes } = require('../core/routes/routines')
|
|
59
59
|
const { authRoutes } = require('../core/routes/auth')
|
|
60
|
+
const { adminRoutes } = require('../core/routes/admin')
|
|
60
61
|
const { variableRoutes } = require('../core/routes/variables')
|
|
61
62
|
const { memoryRoutes } = require('../core/routes/memory')
|
|
62
63
|
const { dailyLogRoutes } = require('../core/routes/daily-logs')
|
|
@@ -221,6 +222,7 @@ async function registerRoutes(app) {
|
|
|
221
222
|
await app.register(workflowRoutes, { workflowRunner })
|
|
222
223
|
await app.register(routineRoutes, { routineRunner })
|
|
223
224
|
await app.register(authRoutes)
|
|
225
|
+
await app.register(adminRoutes)
|
|
224
226
|
await app.register(variableRoutes)
|
|
225
227
|
await app.register(memoryRoutes)
|
|
226
228
|
await app.register(dailyLogRoutes)
|