@agentproto/apps 0.20.2 → 0.21.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/dist/agents-overview/panel.mjs +21 -8
- package/dist/agents-overview/panel.mjs.map +1 -1
- package/dist/agents-overview.mjs +21 -8
- package/dist/agents-overview.mjs.map +1 -1
- package/dist/bin/sync.mjs +19 -2
- package/dist/bin/sync.mjs.map +1 -1
- package/dist/bureau-sessions/panel.mjs +21 -8
- package/dist/bureau-sessions/panel.mjs.map +1 -1
- package/dist/bureau-sessions.mjs +21 -8
- package/dist/bureau-sessions.mjs.map +1 -1
- package/dist/index.mjs +63 -20
- package/dist/index.mjs.map +1 -1
- package/dist/live-session/panel.mjs +21 -8
- package/dist/live-session/panel.mjs.map +1 -1
- package/dist/live-session.mjs +21 -8
- package/dist/live-session.mjs.map +1 -1
- package/dist/ops-panel/ui.d.ts +1 -1
- package/dist/ops-panel/ui.d.ts.map +1 -1
- package/dist/ops-panel.mjs +19 -2
- package/dist/ops-panel.mjs.map +1 -1
- package/dist/panel-bridge.d.ts.map +1 -1
- package/dist/review-panel/panel.d.ts +1 -1
- package/dist/review-panel/panel.d.ts.map +1 -1
- package/dist/review-panel/panel.generated.d.ts +1 -1
- package/dist/review-panel/panel.generated.d.ts.map +1 -1
- package/dist/review-panel/panel.mjs +1 -1
- package/dist/review-panel/panel.mjs.map +1 -1
- package/dist/review-panel.mjs +1 -1
- package/dist/review-panel.mjs.map +1 -1
- package/dist/session-chat/panel.mjs +21 -8
- package/dist/session-chat/panel.mjs.map +1 -1
- package/dist/session-chat.mjs +21 -8
- package/dist/session-chat.mjs.map +1 -1
- package/dist/session-story/panel.mjs +21 -8
- package/dist/session-story/panel.mjs.map +1 -1
- package/dist/session-story.mjs +21 -8
- package/dist/session-story.mjs.map +1 -1
- package/dist/sessions-panel/panel.mjs +21 -8
- package/dist/sessions-panel/panel.mjs.map +1 -1
- package/dist/sessions-panel.mjs +21 -8
- package/dist/sessions-panel.mjs.map +1 -1
- package/dist/store/panel.d.ts +1 -1
- package/dist/store/panel.d.ts.map +1 -1
- package/dist/store/panel.generated.d.ts +1 -1
- package/dist/store/panel.generated.d.ts.map +1 -1
- package/dist/store/panel.mjs +1 -1
- package/dist/store/panel.mjs.map +1 -1
- package/dist/store.mjs +1 -1
- package/dist/store.mjs.map +1 -1
- package/dist/work-board/panel.d.ts +1 -1
- package/dist/work-board/panel.d.ts.map +1 -1
- package/dist/work-board/panel.generated.d.ts +1 -1
- package/dist/work-board/panel.generated.d.ts.map +1 -1
- package/dist/work-board/panel.mjs +21 -8
- package/dist/work-board/panel.mjs.map +1 -1
- package/dist/work-board.mjs +21 -8
- package/dist/work-board.mjs.map +1 -1
- package/package.json +2 -2
- package/repo-maintenance/.agentproto/workflows/maintain/WORKFLOW.md +23 -1
- package/repo-maintenance/.agentproto/workflows/maintain/entry.mjs +101 -9
- package/session-steward/.agentproto/workflows/session-steward/WORKFLOW.md +14 -10
- package/session-steward/.agentproto/workflows/session-steward/cron-rules.mjs +62 -19
- package/session-steward/.agentproto/workflows/session-steward/entry.mjs +40 -11
- package/session-steward/.agentproto/workflows/session-steward/origin-policy.mjs +2 -2
- package/session-steward/README.md +1 -1
- package/session-steward/routines/session-steward-hourly/ROUTINE.md +3 -3
|
@@ -91,11 +91,11 @@ inputs:
|
|
|
91
91
|
type: array
|
|
92
92
|
description: >-
|
|
93
93
|
Origins that may be closed under the current rules. A trailing `*` is a
|
|
94
|
-
prefix wildcard. Default `["cron:*", "gate"]`. Executors (a session with
|
|
94
|
+
prefix wildcard. Default `["cron:*", "gate", "workflow", "review"]`. Executors (a session with
|
|
95
95
|
a `parentSessionId`) are closable regardless.
|
|
96
96
|
items:
|
|
97
97
|
type: string
|
|
98
|
-
default: ["cron:*", "gate"]
|
|
98
|
+
default: ["cron:*", "gate", "workflow", "review"]
|
|
99
99
|
outputs: {}
|
|
100
100
|
steps:
|
|
101
101
|
- id: modelRoles
|
|
@@ -172,6 +172,7 @@ steps:
|
|
|
172
172
|
sessionIds: [$item.sessionId]
|
|
173
173
|
verdict: $item.verdict
|
|
174
174
|
note: $item.note
|
|
175
|
+
wait: true
|
|
175
176
|
|
|
176
177
|
- id: installedApps
|
|
177
178
|
kind: tool
|
|
@@ -388,11 +389,12 @@ steps:
|
|
|
388
389
|
verdict: $item.verdict
|
|
389
390
|
judgedBy: $item.judgedBy
|
|
390
391
|
note: $item.note
|
|
392
|
+
wait: true
|
|
391
393
|
|
|
392
394
|
- id: memoryWriteQueue
|
|
393
395
|
kind: transform
|
|
394
396
|
name: Verdict memory events to append
|
|
395
|
-
description: Entry-based — buildMemoryWriteQueue (a ledger write, never a session action).
|
|
397
|
+
description: Entry-based — buildMemoryWriteQueue (a ledger write, never a session action; empty unless apply).
|
|
396
398
|
|
|
397
399
|
- id: memoryWrite
|
|
398
400
|
kind: map
|
|
@@ -465,8 +467,9 @@ Every rule below is a pure function in `cron-rules.mjs`, pinned by
|
|
|
465
467
|
why (`n live, m busy, k terminal, j excluded`).
|
|
466
468
|
- **Host saturation (9).** If `host_load` is critical, the report lists
|
|
467
469
|
orphans and big non-session processes FIRST — report only, no action.
|
|
468
|
-
- **Verdict memory (10).**
|
|
469
|
-
ledger
|
|
470
|
+
- **Verdict memory (10).** On an `apply: true` pass each verdict is written to
|
|
471
|
+
the app's `app_state` ledger (a dry run reads the ledger but never writes it,
|
|
472
|
+
so its verdicts cannot be served as cached ones to a later real pass); a session judged the same verdict on an unchanged evidence
|
|
470
473
|
fingerprint for `stableVerdictPasses` passes is served from cache and not
|
|
471
474
|
re-judged. Operator disagreements are recorded as examples.
|
|
472
475
|
|
|
@@ -478,10 +481,10 @@ as observed, never nudged.
|
|
|
478
481
|
## Safety
|
|
479
482
|
|
|
480
483
|
- `apply: false` (the default) mutates no SESSION: every session-mutating map
|
|
481
|
-
runs over an empty list
|
|
482
|
-
verdict-memory ledger (`app_state_append`)
|
|
483
|
-
|
|
484
|
-
|
|
484
|
+
runs over an empty list, and a dry run writes nothing else either: the
|
|
485
|
+
append-only verdict-memory ledger (`app_state_append`) is read but only
|
|
486
|
+
appended to on an `apply: true` pass, so a dry run can never influence a later
|
|
487
|
+
real close. Set `appId: ""` to disable the memory entirely.
|
|
485
488
|
- `session_wrapup_apply` re-classifies each id right before acting and always
|
|
486
489
|
refuses `keep`-class ids; this workflow never feeds it one.
|
|
487
490
|
- Rules only ever close `close`/`stuck` ids; a `keepAlive` session is never
|
|
@@ -494,7 +497,8 @@ as observed, never nudged.
|
|
|
494
497
|
`origin`/`parentSessionId` runs through the pure `decideAction`
|
|
495
498
|
(`origin-policy.mjs`): a `userOrigins` match (`chat-starter`, `vscode` by
|
|
496
499
|
default) or a root with no origin and no parent is FLAG-ONLY, even with
|
|
497
|
-
`apply: true` and a confident `done` verdict. `cron:*`, `gate`,
|
|
500
|
+
`apply: true` and a confident `done` verdict. `cron:*`, `gate`, `workflow`
|
|
501
|
+
(workflow-step sessions), `review` (reviewer lanes) and
|
|
498
502
|
executors (a session with a `parentSessionId`) stay closeable. Both lists
|
|
499
503
|
are workflow inputs; a trailing `*` is a prefix wildcard.
|
|
500
504
|
- The report carries an `origin` column and the retained action (e.g.
|
|
@@ -64,7 +64,7 @@ export function isUsefulLoopCommand(signature) {
|
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
const READ_TOOLS = new Set(["read", "cat", "rg", "grep", "sed", "head", "tail", "less", "view", "readfile"])
|
|
67
|
-
const
|
|
67
|
+
const READ_VERB_WORD = /^["']?(cat|rg|grep|sed|head|tail|less|view)["']?$/
|
|
68
68
|
|
|
69
69
|
/**
|
|
70
70
|
* The file a read-like call targeted, or `null`. Best-effort: an in-agent
|
|
@@ -76,15 +76,26 @@ export function readTargetOf(record) {
|
|
|
76
76
|
const tool = String(record?.tool ?? "").toLowerCase()
|
|
77
77
|
const command = str(record?.command)
|
|
78
78
|
const args = Array.isArray(record?.args) ? record.args.map(String) : []
|
|
79
|
-
const
|
|
80
|
-
if (!
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
79
|
+
const readTool = READ_TOOLS.has(tool)
|
|
80
|
+
if (!readTool && command === undefined) return null
|
|
81
|
+
// A shell line is several commands: only a segment that STARTS with a read
|
|
82
|
+
// verb reads a file. `cd <dir> &&`, `git log | head` and `--grep=…` do not.
|
|
83
|
+
const segments = command !== undefined && !readTool ? command.split(/&&|\|\||\||;/) : [undefined]
|
|
84
|
+
for (const seg of segments) {
|
|
85
|
+
let tokens = seg !== undefined ? seg.trim().split(/\s+/) : command !== undefined ? command.split(/\s+/) : args
|
|
86
|
+
if (seg !== undefined) {
|
|
87
|
+
const first = tokens.findIndex((t) => !t.includes("=") || t.startsWith("-"))
|
|
88
|
+
if (first < 0 || !READ_VERB_WORD.test(tokens[first])) continue
|
|
89
|
+
tokens = tokens.slice(first + 1)
|
|
90
|
+
}
|
|
91
|
+
for (const raw of tokens) {
|
|
92
|
+
const t = raw.replace(/^['"]|['"]$/g, "")
|
|
93
|
+
if (!t || t.startsWith("-") || t.includes("=")) continue
|
|
94
|
+
if (t.includes("/") || /\.(md|ts|tsx|js|mjs|cjs|json|txt|py|go|rs|sql|yml|yaml|toml)$/.test(t)) return t
|
|
95
|
+
}
|
|
96
|
+
if (seg === undefined) return args[0] ?? null
|
|
86
97
|
}
|
|
87
|
-
return
|
|
98
|
+
return null
|
|
88
99
|
}
|
|
89
100
|
|
|
90
101
|
function topEntry(counts) {
|
|
@@ -115,7 +126,11 @@ export function detectLoop(records, opts = {}) {
|
|
|
115
126
|
const ts = Date.parse(r?.ts)
|
|
116
127
|
return Number.isFinite(ts) && ts <= nowMs + 1000 && nowMs - ts <= windowMs
|
|
117
128
|
})
|
|
118
|
-
|
|
129
|
+
// A record with no command and no args (an in-agent `read`/`edit` call) has
|
|
130
|
+
// nothing but its tool name to compare, so three of them in ten minutes is
|
|
131
|
+
// normal work, not a loop — leave it out of every repetition signal.
|
|
132
|
+
const informative = inWindow.filter((r) => str(r?.command) !== undefined || (Array.isArray(r?.args) && r.args.length > 0))
|
|
133
|
+
const calls = informative.filter((r) => !isUsefulLoopCommand(callSignature(r)))
|
|
119
134
|
const counts = new Map()
|
|
120
135
|
const reads = new Map()
|
|
121
136
|
for (const r of calls) {
|
|
@@ -146,7 +161,8 @@ export function detectLoop(records, opts = {}) {
|
|
|
146
161
|
ratio: Math.round(ratio * 100) / 100,
|
|
147
162
|
maxVerbatim,
|
|
148
163
|
maxReads,
|
|
149
|
-
usefulExcluded:
|
|
164
|
+
usefulExcluded: informative.length - total,
|
|
165
|
+
anonymousExcluded: inWindow.length - informative.length,
|
|
150
166
|
topCommand: top ? top.key : null,
|
|
151
167
|
topCommandCount: top ? top.count : 0,
|
|
152
168
|
},
|
|
@@ -278,6 +294,9 @@ export function prNumbersOf(session) {
|
|
|
278
294
|
return [...nums].sort((a, b) => a - b)
|
|
279
295
|
}
|
|
280
296
|
|
|
297
|
+
/** Relabel verdict for "no positive evidence either way". */
|
|
298
|
+
export const UNKNOWN_VERDICT = "unknown"
|
|
299
|
+
|
|
281
300
|
const fmtPrs = nums => nums.map(n => `#${n}`).join(", ")
|
|
282
301
|
const isMergedState = state => state === "merged" || state === "MERGED"
|
|
283
302
|
|
|
@@ -285,7 +304,9 @@ const isMergedState = state => state === "merged" || state === "MERGED"
|
|
|
285
304
|
* A terminal session that still carries no derived outcome and no wrapup
|
|
286
305
|
* flag is a relabel CANDIDATE — visible instead of invisible, as the log
|
|
287
306
|
* asks. The proposed verdict is `done` when the session's own record shows a
|
|
288
|
-
* PR (merged, or merely opened — the PR is the hand-off), else `
|
|
307
|
+
* PR (merged, or merely opened — the PR is the hand-off), else `unknown`: a
|
|
308
|
+
* session with no PR is as likely finished as abandoned, and only evidence
|
|
309
|
+
* ({@link refineRelabel}) can say which.
|
|
289
310
|
* `reason` carries the evidence (`PR #1738 merged`, `PRs #1738, #1740 opened`).
|
|
290
311
|
*/
|
|
291
312
|
export function terminalRelabelCandidate(session) {
|
|
@@ -294,14 +315,21 @@ export function terminalRelabelCandidate(session) {
|
|
|
294
315
|
if (session?.wrapupFlag) return { candidate: false, reason: "already flagged" }
|
|
295
316
|
const prs = prNumbersOf(session)
|
|
296
317
|
const wt = session?.worktree?.pr
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
318
|
+
// A worktree can be shared by several sessions, so its merged PR is not
|
|
319
|
+
// proof THIS one finished: a session whose last turn errored is not credited
|
|
320
|
+
// with it, and a PR the session did not record itself is labelled as the
|
|
321
|
+
// worktree's.
|
|
322
|
+
const ownMerged = Number.isInteger(wt?.number) && prs.includes(wt.number)
|
|
323
|
+
if (isMergedState(wt?.state) && (ownMerged || !session?.lastTurnErroredAt)) {
|
|
324
|
+
if (Number.isInteger(wt.number)) {
|
|
325
|
+
return { candidate: true, proposedVerdict: "done", reason: `PR ${fmtPrs([wt.number])} merged${prs.includes(wt.number) ? "" : " (worktree)"}`, prs }
|
|
326
|
+
}
|
|
327
|
+
return { candidate: true, proposedVerdict: "done", reason: prs.length > 0 ? `PR ${fmtPrs(prs)} merged` : "PR merged", prs }
|
|
300
328
|
}
|
|
301
329
|
if (prs.length > 0) {
|
|
302
330
|
return { candidate: true, proposedVerdict: "done", reason: `PR${prs.length > 1 ? "s" : ""} ${fmtPrs(prs)} opened`, prs }
|
|
303
331
|
}
|
|
304
|
-
return { candidate: true, proposedVerdict:
|
|
332
|
+
return { candidate: true, proposedVerdict: UNKNOWN_VERDICT, reason: "terminal, no PR recorded — outcome unknown", prs }
|
|
305
333
|
}
|
|
306
334
|
|
|
307
335
|
/**
|
|
@@ -315,19 +343,34 @@ export function refineRelabel(item, evidence) {
|
|
|
315
343
|
const wt = evidence.worktree?.pr
|
|
316
344
|
const state = wt?.state ?? evidence.pullRequests?.state ?? null
|
|
317
345
|
const known = Array.isArray(item?.prs) ? item.prs : []
|
|
318
|
-
|
|
346
|
+
// `pullRequests.merged` and `worktree.pr` describe the whole worktree, which
|
|
347
|
+
// sibling sessions share: a session that recorded no PR of its own and whose
|
|
348
|
+
// last turn errored is not credited with a sibling's merge.
|
|
349
|
+
const ownPr = known.length > 0 || (evidence.pullRequests?.opened ?? 0) > 0
|
|
350
|
+
const errored = typeof evidence.lastTurnError === "string" && evidence.lastTurnError.trim() !== ""
|
|
351
|
+
const credited = ownPr || !errored
|
|
352
|
+
const merged = credited && (isMergedState(state) || (evidence.pullRequests?.merged ?? 0) > 0)
|
|
319
353
|
if (merged) {
|
|
320
354
|
const n = Number.isInteger(wt?.number) ? wt.number : known.length === 1 ? known[0] : undefined
|
|
321
355
|
const others = known.filter(k => k !== n)
|
|
322
|
-
|
|
356
|
+
// A PR number the session did not record itself is the worktree's.
|
|
357
|
+
const shared = n !== undefined && !known.includes(n) ? " (worktree)" : ""
|
|
358
|
+
const reason = (n !== undefined ? `PR #${n} merged${shared}` : "PR merged") + (others.length > 0 && n !== undefined ? `; also opened ${fmtPrs(others)}` : "")
|
|
323
359
|
return { ...item, proposedVerdict: "done", reason }
|
|
324
360
|
}
|
|
325
361
|
if (item?.proposedVerdict === "done") return item
|
|
326
|
-
if (state === "open" || state === "OPEN") {
|
|
362
|
+
if (credited && (state === "open" || state === "OPEN")) {
|
|
327
363
|
return { ...item, proposedVerdict: "done", reason: Number.isInteger(wt?.number) ? `PR #${wt.number} open` : "PR open" }
|
|
328
364
|
}
|
|
329
365
|
const opened = evidence.pullRequests?.opened ?? 0
|
|
330
366
|
if (opened > 0) return { ...item, proposedVerdict: "done", reason: `${opened} PR${opened > 1 ? "s" : ""} opened` }
|
|
367
|
+
// No PR: `abandoned` needs positive evidence the session did not finish.
|
|
368
|
+
if (item?.proposedVerdict === UNKNOWN_VERDICT) {
|
|
369
|
+
if (typeof evidence.lastTurnError === "string" && evidence.lastTurnError.trim()) {
|
|
370
|
+
return { ...item, proposedVerdict: "abandoned", reason: `no PR, last turn errored: ${evidence.lastTurnError.trim().slice(0, 80)}` }
|
|
371
|
+
}
|
|
372
|
+
if (evidence.turnsCompleted === 0) return { ...item, proposedVerdict: "abandoned", reason: "no PR, no turn ever completed" }
|
|
373
|
+
}
|
|
331
374
|
return item
|
|
332
375
|
}
|
|
333
376
|
|
|
@@ -12,9 +12,10 @@
|
|
|
12
12
|
// RE-CLASSIFIES each id itself immediately before acting and refuses
|
|
13
13
|
// `keep`-class ids outright. On top of that, this workflow:
|
|
14
14
|
// - mutates no SESSION unless `apply` is true (every session-mutating map
|
|
15
|
-
// runs over an empty list otherwise).
|
|
16
|
-
// append-only verdict-memory ledger (`app_state_append`)
|
|
17
|
-
//
|
|
15
|
+
// runs over an empty list otherwise). A dry run writes nothing at all:
|
|
16
|
+
// the append-only verdict-memory ledger (`app_state_append`) is read but
|
|
17
|
+
// only appended to when `apply` is true, so a dry run's verdicts can
|
|
18
|
+
// never be served as cached verdicts to a later real pass;
|
|
18
19
|
// - only ever feeds `close`/`stuck` ids to the rules pass — a `keepAlive`
|
|
19
20
|
// session can only ever be `judge` class, so rules never close it;
|
|
20
21
|
// - drops the caller's own session from every candidate list;
|
|
@@ -664,6 +665,9 @@ export function buildReport(b) {
|
|
|
664
665
|
} else if (memory instanceof Map && memory.size > 0) {
|
|
665
666
|
lines.push(`- verdict memory: ${memory.size} session(s) known` + (cachedCount > 0 ? `, ${cachedCount} served from cache` : ""))
|
|
666
667
|
}
|
|
668
|
+
if (!s.apply && b.steps.memoryApp?.appId) {
|
|
669
|
+
lines.push("- verdict memory was read but not written (dry run)")
|
|
670
|
+
}
|
|
667
671
|
|
|
668
672
|
return lines.join("\n")
|
|
669
673
|
}
|
|
@@ -804,6 +808,24 @@ export function mergeNeverRan(candidates, scan) {
|
|
|
804
808
|
}
|
|
805
809
|
}
|
|
806
810
|
|
|
811
|
+
/** A rule-certain `close` whose session's last turn errored did not finish —
|
|
812
|
+
* parentEnded / merged-worktree only says the parent moved on. Closing it
|
|
813
|
+
* would record `done` on a failed run, so demote it to the judge list. */
|
|
814
|
+
export function demoteErroredCloses(candidates, scan) {
|
|
815
|
+
const errored = new Set((scan?.idle ?? []).filter(r => r?.lastTurnErroredAt).map(r => r.sessionId))
|
|
816
|
+
const close = candidates?.close ?? []
|
|
817
|
+
const demoted = close.filter(e => errored.has(e.sessionId))
|
|
818
|
+
if (demoted.length === 0) return candidates
|
|
819
|
+
return {
|
|
820
|
+
...candidates,
|
|
821
|
+
close: close.filter(e => !errored.has(e.sessionId)),
|
|
822
|
+
judge: [
|
|
823
|
+
...(candidates?.judge ?? []),
|
|
824
|
+
...demoted.map(e => ({ ...e, class: "judge", reasons: [...(e.reasons ?? []), "last turn errored — not auto-closed"] })),
|
|
825
|
+
],
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
|
|
807
829
|
/** `tool_calls_list` map item → the loop verdict + stats for one session. */
|
|
808
830
|
export function analyzeLoopItem(b) {
|
|
809
831
|
const item = b.item ?? {}
|
|
@@ -923,10 +945,12 @@ export function applyRelabelEvidence(relabelQueue, evidenceResult) {
|
|
|
923
945
|
return (relabelQueue ?? []).map(r => (bySession.has(r.sessionId) ? refineRelabel(r, bySession.get(r.sessionId)) : r))
|
|
924
946
|
}
|
|
925
947
|
|
|
926
|
-
/** The `app_state` events to append for this pass's verdicts.
|
|
927
|
-
*
|
|
928
|
-
*
|
|
948
|
+
/** The `app_state` events to append for this pass's verdicts. Written only on
|
|
949
|
+
* an `apply` pass (so streaks accumulate and the cache can engage); a dry run
|
|
950
|
+
* reads memory but never writes it, or its verdicts would be reused by the
|
|
951
|
+
* next real pass instead of being re-judged. */
|
|
929
952
|
export function buildMemoryWriteQueue(finalVerdicts, settings) {
|
|
953
|
+
if (!settings?.apply) return []
|
|
930
954
|
if (!settings?.appId) return []
|
|
931
955
|
const out = []
|
|
932
956
|
for (const r of finalVerdicts ?? []) {
|
|
@@ -976,7 +1000,7 @@ export default {
|
|
|
976
1000
|
appId: { type: "string", description: `Installed app whose \`app_state\` ledger holds the verdict memory. Default ${DEFAULT_APP_ID}.` },
|
|
977
1001
|
stableVerdictPasses: { type: "number", description: `Consecutive passes on an unchanged evidence fingerprint before the judge cache stops re-judging. Default ${DEFAULT_STABLE_VERDICT_PASSES}.`, default: DEFAULT_STABLE_VERDICT_PASSES },
|
|
978
1002
|
userOrigins: { type: "array", description: `Origins that are ALWAYS flag-only, never closed (a human is in the loop). Trailing \`*\` is a prefix wildcard. Default ${JSON.stringify(DEFAULT_USER_ORIGINS)}.`, items: { type: "string" }, default: DEFAULT_USER_ORIGINS },
|
|
979
|
-
closableOrigins: { type: "array", description: `Origins that may be closed under the current rules (cron jobs, gates). Trailing \`*\` is a prefix wildcard. Executors (a session with a parentSessionId) are closable regardless. Default ${JSON.stringify(DEFAULT_CLOSABLE_ORIGINS)}.`, items: { type: "string" }, default: DEFAULT_CLOSABLE_ORIGINS },
|
|
1003
|
+
closableOrigins: { type: "array", description: `Origins that may be closed under the current rules (cron jobs, gates, workflow steps, reviewer lanes). Trailing \`*\` is a prefix wildcard. Executors (a session with a parentSessionId) are closable regardless. Default ${JSON.stringify(DEFAULT_CLOSABLE_ORIGINS)}.`, items: { type: "string" }, default: DEFAULT_CLOSABLE_ORIGINS },
|
|
980
1004
|
},
|
|
981
1005
|
outputs: {},
|
|
982
1006
|
steps: [
|
|
@@ -1000,7 +1024,7 @@ export default {
|
|
|
1000
1024
|
{ id: "liveSessions", kind: "tool", tool: "session_list", inputs: { full: true } },
|
|
1001
1025
|
{ id: "scan", kind: "transform", compute: b => scanLive(b.steps.liveSessions, b.steps.settings, Date.now()) },
|
|
1002
1026
|
// Never-ran 0/0 sessions are `stuck` immediately, never judged (item 3).
|
|
1003
|
-
{ id: "candidatesPlus", kind: "transform", compute: b => mergeNeverRan(b.steps.candidates, b.steps.scan) },
|
|
1027
|
+
{ id: "candidatesPlus", kind: "transform", compute: b => demoteErroredCloses(mergeNeverRan(b.steps.candidates, b.steps.scan), b.steps.scan) },
|
|
1004
1028
|
{ id: "ruleApplyQueue", kind: "transform", compute: b => buildRuleApplyQueue(b.steps.candidatesPlus, b.steps.settings) },
|
|
1005
1029
|
{
|
|
1006
1030
|
// Empty unless `apply` — a dry run dispatches no apply call at all.
|
|
@@ -1014,7 +1038,10 @@ export default {
|
|
|
1014
1038
|
id: "autoApplyOne",
|
|
1015
1039
|
kind: "tool",
|
|
1016
1040
|
tool: "session_wrapup_apply",
|
|
1017
|
-
|
|
1041
|
+
// `wait: true` — past its 25 s default `waitMs` the tool returns a
|
|
1042
|
+
// bare `{ jobId, status: "running" }` and the report would show no
|
|
1043
|
+
// outcome for a session that did get closed.
|
|
1044
|
+
inputs: { sessionIds: ["$item.sessionId"], verdict: "$item.verdict", note: "$item.note", wait: true },
|
|
1018
1045
|
},
|
|
1019
1046
|
],
|
|
1020
1047
|
},
|
|
@@ -1205,12 +1232,14 @@ export default {
|
|
|
1205
1232
|
id: "judgedApplyOne",
|
|
1206
1233
|
kind: "tool",
|
|
1207
1234
|
tool: "session_wrapup_apply",
|
|
1208
|
-
|
|
1235
|
+
// `wait: true`: same as autoApplyOne.
|
|
1236
|
+
inputs: { sessionIds: ["$item.sessionId"], verdict: "$item.verdict", judgedBy: "$item.judgedBy", note: "$item.note", wait: true },
|
|
1209
1237
|
},
|
|
1210
1238
|
],
|
|
1211
1239
|
},
|
|
1212
1240
|
// Verdict memory write-back (item 10) — a ledger append, never a session
|
|
1213
|
-
// action; best-effort (an uninstalled app just
|
|
1241
|
+
// action; empty unless `apply`; best-effort (an uninstalled app just
|
|
1242
|
+
// yields no memory).
|
|
1214
1243
|
{ id: "memoryWriteQueue", kind: "transform", compute: b => buildMemoryWriteQueue(b.steps.finalVerdicts, { ...b.steps.settings, appId: b.steps.memoryApp?.appId ?? "" }) },
|
|
1215
1244
|
{
|
|
1216
1245
|
id: "memoryWrite",
|
|
@@ -12,7 +12,7 @@ export const DEFAULT_USER_ORIGINS = ["chat-starter", "vscode"]
|
|
|
12
12
|
/** Origins the steward MAY close under the current rules. `cron:*` matches
|
|
13
13
|
* every cron-spawned job (`origin: "cron:<jobId>"`); `gate` matches
|
|
14
14
|
* supervision-gate sessions. */
|
|
15
|
-
export const DEFAULT_CLOSABLE_ORIGINS = ["cron:*", "gate"]
|
|
15
|
+
export const DEFAULT_CLOSABLE_ORIGINS = ["cron:*", "gate", "workflow", "review"]
|
|
16
16
|
|
|
17
17
|
/** The reason shown (and recorded) when a would-be close is bounded by origin. */
|
|
18
18
|
export const USER_ORIGIN_REASON = "flag (origine utilisateur)"
|
|
@@ -46,7 +46,7 @@ export function resolveOriginPolicy(policy) {
|
|
|
46
46
|
/** The origin class of a candidate:
|
|
47
47
|
* - `"user"` — human-launched: a `userOrigins` match, OR a root with no
|
|
48
48
|
* origin and no parent. FLAG ONLY, never close.
|
|
49
|
-
* - `"closable"` — a `closableOrigins` match (cron:*, gate) or an executor
|
|
49
|
+
* - `"closable"` — a `closableOrigins` match (cron:*, gate, workflow, review) or an executor
|
|
50
50
|
* (has a `parentSessionId`). Close allowed under the current rules.
|
|
51
51
|
* `userOrigins` wins over both `closableOrigins` and the executor rule, so a
|
|
52
52
|
* `vscode` executor is still user-origin. An unrecognized root origin is
|
|
@@ -53,7 +53,7 @@ the `userOrigins` / `closableOrigins` inputs:
|
|
|
53
53
|
`origin` and no `parentSessionId` (a human launched it). A would-be close —
|
|
54
54
|
even a rule-certain `close`/`stuck`, even a confident `done` — is recorded
|
|
55
55
|
as a `needs-input` flag with reason `flag (origine utilisateur)`.
|
|
56
|
-
- **Close allowed:** `cron:*`, `gate`, and executors (a session with a
|
|
56
|
+
- **Close allowed:** `cron:*`, `gate`, `workflow` (step sessions), `review` (reviewer lanes), and executors (a session with a
|
|
57
57
|
`parentSessionId`).
|
|
58
58
|
|
|
59
59
|
A trailing `*` in either list is a prefix wildcard. The report carries the
|
|
@@ -23,10 +23,10 @@ target:
|
|
|
23
23
|
askSessions: false
|
|
24
24
|
# Origin policy (the committed default): never close a human's session.
|
|
25
25
|
# `chat-starter`/`vscode` (and any root with no origin and no parent) are
|
|
26
|
-
# FLAG-ONLY; `cron:*` jobs, `gate` sessions, and executors (a session with
|
|
26
|
+
# FLAG-ONLY; `cron:*` jobs, `gate`, `workflow` and `review` sessions, and executors (a session with
|
|
27
27
|
# a parentSessionId) stay closeable. A trailing `*` is a prefix wildcard.
|
|
28
28
|
userOrigins: ["chat-starter", "vscode"]
|
|
29
|
-
closableOrigins: ["cron:*", "gate"]
|
|
29
|
+
closableOrigins: ["cron:*", "gate", "workflow", "review"]
|
|
30
30
|
retry:
|
|
31
31
|
max_attempts: 1
|
|
32
32
|
backoff: fixed
|
|
@@ -68,7 +68,7 @@ The steward bounds every action by the candidate's `origin` (pure
|
|
|
68
68
|
`origin` and no `parentSessionId` (a human launched it). A would-be close —
|
|
69
69
|
even a rule-certain `close`/`stuck`, even a confident `done` — becomes a
|
|
70
70
|
`needs-input` flag with reason `flag (origine utilisateur)`.
|
|
71
|
-
- **Close allowed:** `cron:*` (any cron job), `gate`, and executors (a
|
|
71
|
+
- **Close allowed:** `cron:*` (any cron job), `gate`, `workflow` (workflow-step sessions), `review` (reviewer lanes), and executors (a
|
|
72
72
|
session with a `parentSessionId`).
|
|
73
73
|
|
|
74
74
|
Both lists are workflow inputs (`userOrigins`, `closableOrigins`); the values
|