create-pmos 0.214.0 → 0.216.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/package.json +1 -1
- package/payload/KIT-VERSION +1 -1
- package/payload/planning/evals/eval-calibration.md +1 -1
- package/payload/planning/okrs/health-budgets.md +1 -1
- package/payload/state/discoveries.md +1 -1
- package/workspace/c3-sessions-panel-test.js +69 -11
- package/workspace/c3-work-test.js +2 -2
- package/workspace/guide/console3-sessions.md +33 -9
- package/workspace/guide/my-work.md +5 -2
- package/workspace/lib/day-run.js +30 -1
- package/workspace/lib/my-work.js +1 -1
- package/workspace/lib/proactive-cli.js +8 -2
- package/workspace/lib/routes-my-work.js +44 -17
- package/workspace/lib/routes-pm-items.js +8 -0
- package/workspace/lib/transcript-read.js +93 -10
- package/workspace/lib/work-assigned.js +23 -11
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/public/pm/c3-case.js +8 -2
- package/workspace/public/pm/c3-session-conv.js +164 -0
- package/workspace/public/pm/c3-session-term.js +63 -16
- package/workspace/public/pm/c3-settings.js +1 -1
- package/workspace/public/pm/console3.css +67 -0
- package/workspace/public/pm/pm-work-view.js +44 -22
- package/workspace/public/pm/tab-term.js +9 -0
- package/workspace/server.js +32 -12
- package/workspace/skills/day-task.skill +1 -0
- package/workspace/test-files.json +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-pmos",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.216.0",
|
|
4
4
|
"description": "Scaffold the PMOS portable kit — run the PMOS product-management workflow (initiative → lane-fit spec → contract → build → three gates, with work-type lanes for features, research, maintenance, incidents, and content) entirely inside your own repo, with all state as git-versioned files. Includes `workflow-sweep`, which audits the state every board derives from and separates real defects from work you simply have not done. `create-pmos workspace` also installs + launches the local Terminal Workspace GUI. No database, no network, no telemetry.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"bin": {
|
package/payload/KIT-VERSION
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
type: Reference
|
|
3
3
|
title: "Eval calibration log"
|
|
4
4
|
description: "Append-only record of every PM override of an evaluator verdict."
|
|
5
|
-
generated: {by: process:create-pmos, at: "2026-10-
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-10-14T04:16:13Z"}
|
|
6
6
|
status: stable
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
type: Reference
|
|
3
3
|
title: "Health budgets"
|
|
4
4
|
description: "Closed registry of health-budget anchors (D59), edited only by the PM."
|
|
5
|
-
generated: {by: process:create-pmos, at: "2026-10-
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-10-14T04:16:13Z"}
|
|
6
6
|
status: stable
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
type: Reference
|
|
3
3
|
title: "Discoveries and friction log"
|
|
4
4
|
description: "Append-only record of what runs discovered and where the process rubbed."
|
|
5
|
-
generated: {by: process:create-pmos, at: "2026-10-
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-10-14T04:16:13Z"}
|
|
6
6
|
status: stable
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -37,6 +37,17 @@ async function openPanel(page) {
|
|
|
37
37
|
if (got && await page.$('[data-c3-all-sessions]')) await page.click('[data-c3-all-sessions]');
|
|
38
38
|
}
|
|
39
39
|
async function waitFor(page, fn, arg, ms = 15000) { try { await page.waitForFunction(fn, arg, { timeout: ms }); return true; } catch { return false; } }
|
|
40
|
+
/* the session sheet opens on the conversation (c3-session-conv.js); Terminal shows the live terminal */
|
|
41
|
+
const sheetOf = (i) => `[data-c3-session="${i}"]`;
|
|
42
|
+
async function toTerm(page, i) {
|
|
43
|
+
if (!await waitFor(page, (s) => !!document.querySelector(s), sheetOf(i))) return false;
|
|
44
|
+
await page.click('[data-c3-view="term"]');
|
|
45
|
+
return waitFor(page, (x) => !!document.querySelector(`[data-c3-session-term="${x}"] .xterm`), i);
|
|
46
|
+
}
|
|
47
|
+
async function openTerm(page, i) {
|
|
48
|
+
await page.evaluate((x) => window.dispatchEvent(new CustomEvent('pmos:open-session', { detail: { id: x } })), i);
|
|
49
|
+
return toTerm(page, i);
|
|
50
|
+
}
|
|
40
51
|
const get = (p) => fetch(BASE + p).then((r) => r.json());
|
|
41
52
|
// the agent CLI's home for this run: the conversation test writes a session record and a transcript here, as claude would
|
|
42
53
|
const CLAUDE_HOME = path.join(T.STATE_DIR, 'claude-home');
|
|
@@ -104,23 +115,30 @@ function startShell(name) {
|
|
|
104
115
|
/* ── C: Open the session stays on this page ── */
|
|
105
116
|
const url0 = page.url();
|
|
106
117
|
await page.click(`[data-blank-panel="${id}"] [data-open-session]`);
|
|
107
|
-
ok('C1 the session opens in a
|
|
118
|
+
ok('C1 the session opens in a sheet, on its conversation', await waitFor(page, (i) => { const c = document.querySelector(`[data-c3-session="${i}"] [data-c3-conv]`); return !!c && !c.hidden && !document.querySelector('.xterm'); }, id));
|
|
119
|
+
ok('C1b a shell with no Claude in it says so and offers Start Claude here', await waitFor(page, () => /No Claude in this shell yet/.test((document.querySelector('[data-cv="none"]') || {}).textContent || '') && !!document.querySelector('[data-cv="start-claude"]')));
|
|
120
|
+
ok('C1c the view control has Conversation and Terminal, Conversation pressed', await page.evaluate(() => [...document.querySelectorAll('[data-c3-view]')].map((b) => b.dataset.c3View + ':' + b.getAttribute('aria-pressed')).join() === 'conv:true,term:false'));
|
|
121
|
+
ok('C1d Terminal shows the live terminal in the same sheet', await toTerm(page, id) && await page.evaluate(() => document.querySelector('[data-c3-conv]').hidden === true));
|
|
108
122
|
ok('C2 the page stays on console3', page.url() === url0, page.url());
|
|
109
123
|
await sleep(800);
|
|
110
124
|
await page.click(`[data-c3-session-term="${id}"] .xterm`);
|
|
111
|
-
|
|
125
|
+
// a claude that only echoes, so Start Claude here can be checked without a real agent
|
|
126
|
+
await page.keyboard.type('claude() { echo c3-start-$((3*3)); }; echo c3-typed-$((40+2))\n');
|
|
112
127
|
ok('C2b what is typed in the sheet reaches the shell', !!(await until(async () => /c3-typed-42/.test((await get('/api/sessions/transcript?id=' + id + '&max=4000')).text || ''), 8000)));
|
|
113
128
|
await page.keyboard.press('Escape');
|
|
114
129
|
await sleep(300);
|
|
115
130
|
ok('C3 Esc in the terminal goes to the shell, not the sheet', await page.evaluate((i) => !!document.querySelector(`[data-c3-session-term="${i}"]`), id));
|
|
116
131
|
await page.keyboard.press('Control+u'); // the shell read that Esc as a Meta prefix: this key takes it, so the next line starts clean
|
|
132
|
+
await page.click('[data-c3-view="conv"]');
|
|
133
|
+
ok('C3b Conversation comes back, the terminal stays mounted but hidden', await waitFor(page, (i) => { const c = document.querySelector('[data-c3-conv]'); const t = document.querySelector(`[data-c3-session-term="${i}"]`); return !!c && !c.hidden && !!t && !!t.querySelector('.xterm') && t.closest('[data-c3-view-pane]').hidden; }, id));
|
|
134
|
+
await page.click('[data-cv="start-claude"]');
|
|
135
|
+
ok('C3c Start Claude here types claude into the shell', !!(await until(async () => /c3-start-9/.test((await get('/api/sessions/transcript?id=' + id + '&max=4000')).text || ''), 8000)));
|
|
117
136
|
await page.click('[data-shell="sheet"] [aria-label="Close"]');
|
|
118
137
|
ok('C4 the close button closes the terminal sheet', await waitFor(page, () => !document.querySelector('[data-c3-session-term]')));
|
|
119
138
|
ok('C5 closing the sheet leaves the session running', await until(async () => ((await get('/api/sessions')).sessions || []).some((s) => s.id === id && s.state !== 'exited')));
|
|
120
139
|
|
|
121
140
|
/* ── G: the scratchpad and the size control on the terminal sheet (c3-term-pad-size) ── */
|
|
122
|
-
|
|
123
|
-
const g0 = await waitFor(page, (i) => !!document.querySelector(`[data-c3-session-term="${i}"] .xterm`), id);
|
|
141
|
+
const g0 = await openTerm(page, id);
|
|
124
142
|
ok('G0 the terminal sheet opens for the pad checks', g0 || !!(await page.$('[data-c3-size-seg]')));
|
|
125
143
|
const sizes = await page.$$eval('[data-c3-size-seg] [data-c3-size]', (b) => b.map((x) => x.dataset.c3Size));
|
|
126
144
|
ok('G1 the size control has 4 options', sizes.join() === 'normal,half,two-thirds,whole', sizes.join());
|
|
@@ -137,8 +155,7 @@ function startShell(name) {
|
|
|
137
155
|
await page.click('[data-c3-size="half"]');
|
|
138
156
|
await page.click('[data-shell="sheet"] [aria-label="Close"]');
|
|
139
157
|
await waitFor(page, () => !document.querySelector('[data-c3-session-term]'));
|
|
140
|
-
await page
|
|
141
|
-
await waitFor(page, (i) => !!document.querySelector(`[data-c3-session-term="${i}"] .xterm`), id);
|
|
158
|
+
await openTerm(page, id);
|
|
142
159
|
await sleep(200);
|
|
143
160
|
ok('G4 the size choice is kept after closing and opening again', Math.abs((await wid()) - vw / 2) < 3, String(await wid()));
|
|
144
161
|
ok('G4b it is kept in localStorage', await page.evaluate(() => localStorage.getItem('pmos.workspace.c3.term-size') === 'half'));
|
|
@@ -158,7 +175,7 @@ function startShell(name) {
|
|
|
158
175
|
await page.fill('[data-c3-pad="text"]', 'flushed-on-close');
|
|
159
176
|
await page.click('[data-shell="sheet"] [aria-label="Close"]');
|
|
160
177
|
ok('G8 closing the sheet saves a pad edit at once', !!(await until(async () => (await get('/api/scratchpad?session=' + id)).text === 'flushed-on-close', 2000)));
|
|
161
|
-
await page
|
|
178
|
+
await openTerm(page, id);
|
|
162
179
|
await waitFor(page, () => !!document.querySelector('[data-c3-pad="text"]'));
|
|
163
180
|
ok('G9 the pad loads its text when the terminal opens again', await waitFor(page, () => document.querySelector('[data-c3-pad="text"]').value === 'flushed-on-close'));
|
|
164
181
|
/* the grip above the pad resizes it: a drag up makes the pad taller and the terminal shorter; the height is kept */
|
|
@@ -183,9 +200,9 @@ function startShell(name) {
|
|
|
183
200
|
/* ── D: End asks first, then ends ── */
|
|
184
201
|
// Sessions opens the terminal of the session opened last while it runs (PM, 2026-10-13); its "All sessions" opens the panel
|
|
185
202
|
await page.click('[data-c3pg="agent-button"]');
|
|
186
|
-
ok('D0 Sessions opens the
|
|
203
|
+
ok('D0 Sessions opens the session opened last', await waitFor(page, (s) => !!document.querySelector(s + ' [data-c3-conv]'), sheetOf(id)));
|
|
187
204
|
await page.click('[data-c3-all-sessions]');
|
|
188
|
-
ok('D0a All sessions in the
|
|
205
|
+
ok('D0a All sessions in the session sheet opens the panel', await waitFor(page, (s) => !!document.querySelector(s), sel) && !(await page.$('[data-c3-session]')));
|
|
189
206
|
ok('D0b it opens on the session opened last: its row is open and marked', await waitFor(page, (i) => { const li = document.querySelector(`[data-c3t="mine"] [data-run="${i}"]`); const end = document.querySelector(`[data-end-session="${i}"]`); return !!li && li.hasAttribute('data-last-open') && !!end && end.offsetParent !== null; }, id)
|
|
190
207
|
&& await page.evaluate((i) => localStorage.getItem('pmos.workspace.c3.last-session') === i, id));
|
|
191
208
|
if (!(await page.isVisible(`[data-end-session="${id}"]`).catch(() => false))) await page.click(sel + ' .nb');
|
|
@@ -232,6 +249,47 @@ function startShell(name) {
|
|
|
232
249
|
return me && tool && !!bot && !!bot.querySelector('li b') && !!bot.querySelector('li code') && !/not shown/.test(f.textContent);
|
|
233
250
|
}, feedSel, 10000));
|
|
234
251
|
|
|
252
|
+
/* ── S: the session sheet's conversation, from the same transcript ── */
|
|
253
|
+
await page.evaluate((i) => window.dispatchEvent(new CustomEvent('pmos:open-session', { detail: { id: i } })), id);
|
|
254
|
+
const cvSel = sheetOf(id) + ' [data-c3-conv]';
|
|
255
|
+
ok('S1 the sheet shows the prompt, a folded tool line and the reply', await waitFor(page, (s) => {
|
|
256
|
+
const f = document.querySelector(s); if (!f) return false;
|
|
257
|
+
const me = [...f.querySelectorAll('[data-chat="me"]')].some((n) => n.textContent === 'What changed today?');
|
|
258
|
+
const tool = f.querySelector('details[data-chat="tool"]');
|
|
259
|
+
const bot = f.querySelector('[data-chat="bot"]');
|
|
260
|
+
return me && !!tool && !tool.open && /Used Bash/.test(tool.textContent) && !!bot && !!bot.querySelector('li b') && !/not shown/.test(f.textContent);
|
|
261
|
+
}, cvSel, 10000));
|
|
262
|
+
ok('S2 the tool line lists each call with its target', await page.evaluate((s) => [...document.querySelectorAll(s + ' details[data-chat="tool"] li')].map((li) => li.querySelector('b').textContent + ' ' + ((li.querySelector('span') || {}).textContent || '')).join() === 'Bash git log', cvSel));
|
|
263
|
+
ok('S3 the status reads Your turn and the meta names the model', await page.evaluate(() => (document.querySelector('[data-cv="status"]') || {}).textContent === 'Your turn' && /claude-test/.test((document.querySelector('[data-cv="meta"]') || {}).textContent || '')));
|
|
264
|
+
await page.fill('[data-cv="msg"]', 'echo c3-conv-$((5*5))');
|
|
265
|
+
await page.press('[data-cv="msg"]', 'Enter');
|
|
266
|
+
ok('S4 a sent message shows at once', await waitFor(page, () => [...document.querySelectorAll('[data-c3-conv] [data-chat="me"]')].some((n) => /c3-conv-/.test(n.textContent)), null, 3000));
|
|
267
|
+
ok('S4b the send box types into the session and presses Enter', !!(await until(async () => /^c3-conv-25/m.test((await get('/api/sessions/transcript?id=' + id + '&max=4000')).text || ''), 8000)));
|
|
268
|
+
// Claude asks before a tool: the transcript holds an open call and the pane shows Claude Code's prompt
|
|
269
|
+
fs.appendFileSync(path.join(proj, 'conv-test-1.jsonl'), rec({ type: 'assistant', message: { id: 'msg_3', model: 'claude-test', role: 'assistant', content: [{ type: 'tool_use', id: 't2', name: 'Bash', input: { command: 'rm notes.txt' } }] } }) + '\n');
|
|
270
|
+
await page.fill('[data-cv="msg"]', "clear; printf 'Do you want to proceed?\\n> 1. Yes\\n 2. No, and tell Claude what to do differently\\n'");
|
|
271
|
+
await page.press('[data-cv="msg"]', 'Enter');
|
|
272
|
+
ok('S5 the allow prompt shows as a card with its options', await waitFor(page, () => {
|
|
273
|
+
const c = document.querySelector('[data-c3-conv] [data-cv="ask"]'); if (!c) return false;
|
|
274
|
+
const o = [...c.querySelectorAll('[data-cv-opt]')].map((b) => b.dataset.cvOpt + ':' + b.textContent).join('|');
|
|
275
|
+
return /Do you want to proceed\?/.test(c.textContent) && o === '1:Yes|2:No, and tell Claude what to do differently' && !!c.querySelector('[data-cv="ask-term"]');
|
|
276
|
+
}, null, 20000));
|
|
277
|
+
if (!results[results.length - 1].cond) { const c = await get('/api/sessions/conversation?id=' + id); console.log(' conversation:', JSON.stringify({ state: c.state, turn: c.turn, openCall: c.openCall, ask: c.ask }), JSON.stringify(((await get('/api/sessions/transcript?id=' + id + '&max=600')).text || '').slice(-300))); }
|
|
278
|
+
ok('S5b the status reads Needs you', await page.evaluate(() => (document.querySelector('[data-cv="status"]') || {}).textContent === 'Needs you'));
|
|
279
|
+
await page.click('[data-cv-opt="2"]', { timeout: 3000 }).catch(() => {});
|
|
280
|
+
await sleep(500);
|
|
281
|
+
await page.fill('[data-cv="msg"]', '; echo c3-opt-$((2*4))');
|
|
282
|
+
await page.press('[data-cv="msg"]', 'Enter');
|
|
283
|
+
ok('S5c an option button types its number into the session', !!(await until(async () => /2; echo c3-opt/.test((await get('/api/sessions/transcript?id=' + id + '&max=6000')).text || ''), 8000)));
|
|
284
|
+
await page.click('[data-cv="ask-term"]').catch(() => {});
|
|
285
|
+
ok('S6 Answer in the terminal switches to the terminal', await waitFor(page, (i) => !!document.querySelector(`[data-c3-session-term="${i}"] .xterm`) && document.querySelector('[data-c3-view="term"]').getAttribute('aria-pressed') === 'true', id));
|
|
286
|
+
await page.click('[data-shell="sheet"] [aria-label="Close"]');
|
|
287
|
+
await waitFor(page, () => !document.querySelector('[data-c3-session]'));
|
|
288
|
+
// the panel row was open before the sheet: open it again for End
|
|
289
|
+
// (it opens on the session opened last, with its row open)
|
|
290
|
+
await openPanel(page);
|
|
291
|
+
if (!await waitFor(page, (i) => { const b = document.querySelector(`[data-end-session="${i}"]`); return !!b && b.offsetParent !== null; }, id, 5000)) await page.click(sel + ' .nb');
|
|
292
|
+
|
|
235
293
|
await page.click(`[data-end-session="${id}"]`);
|
|
236
294
|
await page.click(`[data-end-yes="${id}"]`);
|
|
237
295
|
const gone = async () => !((await get('/api/sessions')).sessions || []).some((s) => s.id === id);
|
|
@@ -255,9 +313,9 @@ function startShell(name) {
|
|
|
255
313
|
const made = await until(async () => ((await get('/api/sessions')).sessions || []).find((s) => !before.has(s.id) && s.name === 'c3 new shell'), 10000);
|
|
256
314
|
ok('F3 Start starts the session on the server', !!made);
|
|
257
315
|
ok('F3b it starts on the board chosen in the sheet', !!made && made.ws === opts.find((o) => o.on).v, JSON.stringify(made && made.ws));
|
|
258
|
-
ok('F4
|
|
316
|
+
ok('F4 it opens in a sheet on console3', !!made && await waitFor(page, (s) => !!document.querySelector(s + ' [data-c3-conv]'), sheetOf(made.id)) && page.url() === url0, page.url());
|
|
259
317
|
await page.click('[data-shell="sheet"] [aria-label="Close"]');
|
|
260
|
-
await waitFor(page, () => !document.querySelector('[data-c3-session
|
|
318
|
+
await waitFor(page, () => !document.querySelector('[data-c3-session]'));
|
|
261
319
|
await page.evaluate(() => window.dispatchEvent(new CustomEvent('pmos:open-session', { detail: {} })));
|
|
262
320
|
ok('F5 an open-session request with no session opens the new-session sheet, not the classic app', await waitFor(page, () => !!document.querySelector('[data-c3ns="form"]')) && page.url() === url0, page.url());
|
|
263
321
|
await page.click('[data-shell="sheet"] [aria-label="Close"]');
|
|
@@ -87,8 +87,8 @@ const W = '[data-pm-work]';
|
|
|
87
87
|
if (!ready) return finish(1);
|
|
88
88
|
|
|
89
89
|
/* the six views and their counts (R1) */
|
|
90
|
-
|
|
91
|
-
|
|
90
|
+
// only the bar on screen: a redraw puts a new bar in the header, hidden there (.calbar.qt-moved), until the quiet layout moves it beside Add work
|
|
91
|
+
const readBar = () => st((w) => [...document.querySelectorAll(w + ' [data-pm-lenses] [data-lens]')].filter((b) => b.offsetParent !== null).map((b) => [b.getAttribute('data-lens'), b.textContent.replace(/\d+$/, '').trim()]), W);
|
|
92
92
|
await until(async () => (await readBar()).length === VIEWS.length, 10000, 100);
|
|
93
93
|
const bar = await readBar();
|
|
94
94
|
ok('the bar has the six views in order: Inbox, Today, Upcoming, Waiting, Someday, Review', JSON.stringify(bar.map((b) => b[0])) === JSON.stringify(VIEWS) && JSON.stringify(bar.map((b) => b[1])) === JSON.stringify(LABELS), JSON.stringify(bar));
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
---
|
|
2
2
|
type: Guide
|
|
3
3
|
title: "Sessions in console3: start, open, message, read and end your sessions"
|
|
4
|
-
description: "How the Sessions panel in console3 groups your sessions, how to start a session, how to open a session
|
|
5
|
-
timestamp: 2026-10-
|
|
4
|
+
description: "How the Sessions panel in console3 groups your sessions, how to start a session, how to open a session as a conversation or as its terminal without leaving console3, how to send a message to a session, and how to end a session."
|
|
5
|
+
timestamp: 2026-10-14
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Sessions in console3: start, open, message, read and end your sessions
|
|
9
9
|
|
|
10
10
|
Press the **Sessions** button in the console3 header to open the panel. It is the bolt icon with no label. Point at it to see its name. When a session needs you, a count shows on it. It shows your sessions in four groups: **On a card**, **Started by me**, **Past (7 days)** and **Automatic**.
|
|
11
11
|
|
|
12
|
-
When the session that you opened last is still running, the **Sessions** button opens
|
|
12
|
+
When the session that you opened last is still running, the **Sessions** button opens that session directly, on its conversation. To see all your sessions, press **All sessions** at the top of the session sheet. If that session has ended, the button opens the panel.
|
|
13
13
|
|
|
14
14
|
The panel opens on the session that you opened last. Its row is open, so you can read the conversation and send a message without searching for it. A session counts as opened when you open its row or its terminal.
|
|
15
15
|
|
|
@@ -32,7 +32,7 @@ You start a session in console3. You do not need the classic app.
|
|
|
32
32
|
- **Automatic**: the server selects the tree.
|
|
33
33
|
- A listed tree: the session runs in that working tree.
|
|
34
34
|
- **New branch**: type the branch name. The session runs in its own working copy of the new branch.
|
|
35
|
-
6. Press **Start**. The session starts, and
|
|
35
|
+
6. Press **Start**. The session starts, and it opens in a sheet on the same page. If you selected a different board, the dock changes to that board.
|
|
36
36
|
|
|
37
37
|
If the server cannot start the session, the sheet shows the reason.
|
|
38
38
|
|
|
@@ -47,7 +47,31 @@ If the server finds that the process of a session stopped (for example, after a
|
|
|
47
47
|
1. Press the row to open it.
|
|
48
48
|
2. Press **Open the session**.
|
|
49
49
|
|
|
50
|
-
The
|
|
50
|
+
The session opens in a sheet on the same page, on its conversation. To close the sheet, press its close button. The session continues to run.
|
|
51
|
+
|
|
52
|
+
Two buttons at the top of the sheet change the view:
|
|
53
|
+
|
|
54
|
+
- **Conversation**: your messages and the replies of Claude, with a box to send a message. This is the view that opens first.
|
|
55
|
+
- **Terminal**: the live terminal of the session. You can type in it. When the terminal has focus, Esc and Tab go to the terminal. Use it for the things that the conversation does not show, for example a menu of Claude Code or the full output of a command.
|
|
56
|
+
|
|
57
|
+
Press **Conversation** to go back. The terminal stays connected while the sheet is open, so nothing restarts when you change the view.
|
|
58
|
+
|
|
59
|
+
### The conversation
|
|
60
|
+
|
|
61
|
+
The conversation comes from the transcript that Claude Code writes on this computer. It shows the words of Claude as Claude wrote them. Nothing is summarised, and nothing is sent to a different place.
|
|
62
|
+
|
|
63
|
+
- Your messages are on the right. The replies of Claude are on the left. Lists, bold text and code show as formatted text.
|
|
64
|
+
- A grey line, for example "Used Bash, Read ×2", shows a run of tool calls. Press it to see each call and its target, for example the file or the command.
|
|
65
|
+
- While Claude works, a line says so. Press the stop button next to the message box to stop Claude. It types Esc into the session.
|
|
66
|
+
- When Claude asks before it uses a tool, a card shows the question and its options, for example **Yes** and **No**. Press an option to answer. You can also press **Answer in the terminal**.
|
|
67
|
+
- Type a message in the box and press Enter to send it. Shift Enter starts a new line. **/compact** and **/clear** put that command in the box.
|
|
68
|
+
- The tag above the conversation shows the state: **Working**, **Needs you**, **Your turn**, **Shell** or **Ended**.
|
|
69
|
+
|
|
70
|
+
A shell where Claude has not started says "No Claude in this shell yet". Press **Start Claude here** to start Claude in that shell. Its conversation then shows in this view.
|
|
71
|
+
|
|
72
|
+
If PMOS cannot find the transcript of a Claude session, the view says so, and it does not show the transcript of a different session. Read the session in the terminal. PMOS looks for transcripts in `~/.claude`, in each `~/.claude-*` profile folder, and in the folder that `CLAUDE_CONFIG_DIR` names.
|
|
73
|
+
|
|
74
|
+
At **2/3** and **Whole**, the sheet also shows a list of your running sessions at the left. Press a session to open it.
|
|
51
75
|
|
|
52
76
|
### The scratchpad
|
|
53
77
|
|
|
@@ -58,11 +82,11 @@ Below the terminal is a scratchpad. It keeps text for this session only. The tex
|
|
|
58
82
|
- To make the pad taller or shorter, drag the grip on the line above it. Or select the grip and press the up arrow or the down arrow. The terminal gets the remaining room. The page remembers the height. To go back to the default height, double-click the grip.
|
|
59
83
|
- Esc in the pad closes the panel. Esc in the terminal goes to the terminal.
|
|
60
84
|
|
|
61
|
-
### The size of the
|
|
85
|
+
### The size of the sheet
|
|
62
86
|
|
|
63
|
-
Four buttons in the header of the
|
|
87
|
+
Four buttons in the header of the sheet set its width: **Normal**, **Half**, **2/3** and **Whole**. Half is half of the window. 2/3 is two thirds of the window. Whole is the full window. The terminal fits the new size. The page remembers your choice for the next session you open. On a phone the panel is already full width, so the buttons are not shown.
|
|
64
88
|
|
|
65
|
-
The **Open the session** link in the sessions dock opens the same
|
|
89
|
+
The **Open the session** link in the sessions dock opens the same sheet.
|
|
66
90
|
|
|
67
91
|
## Send a message to a session
|
|
68
92
|
|
|
@@ -78,7 +102,7 @@ The panel shows the conversation of a Claude session:
|
|
|
78
102
|
|
|
79
103
|
- Your messages are on the right.
|
|
80
104
|
- The replies of Claude are on the left. Lists, bold text and code show as formatted text.
|
|
81
|
-
- A grey line, for example "Used Bash, Read ×2", shows the tools that Claude used. The
|
|
105
|
+
- A grey line, for example "Used Bash, Read ×2", shows the tools that Claude used. The session sheet also shows the target of each call.
|
|
82
106
|
- While Claude works, a grey line says so.
|
|
83
107
|
|
|
84
108
|
The panel reads the conversation from the transcript that Claude Code writes on this computer. Nothing is sent to a different place. The panel updates while it is open.
|
|
@@ -100,7 +100,7 @@ An item goes to the first view that fits these rules, in this order. Parked is S
|
|
|
100
100
|
|
|
101
101
|
Open an item that shows **Agent can:**. The case file shows **What the agent will do**.
|
|
102
102
|
|
|
103
|
-
The offer is on Jira and WBS items with a suggestion, and on each item the morning brief suggests for you (for example a Slack reply or an email). On a brief item the offer is the reply or the note that the item asks for, and the draft goes back to the place the item came from, only when you post it.
|
|
103
|
+
The offer is on Jira and WBS items with a suggestion, and on each item the morning brief suggests for you (for example a Slack reply or an email), also on the days after, while you keep the item in Work. On a brief item the offer is the reply or the note that the item asks for, and the draft goes back to the place the item came from, only when you post it.
|
|
104
104
|
|
|
105
105
|
An item you added yourself shows **Agent can draft a first version**. The agent drafts what the item asks for (a document, a plan or a recommendation), and the draft stays on the item. It has no place to post to: read it, keep it, or send it back with a note.
|
|
106
106
|
|
|
@@ -121,11 +121,14 @@ The row chip then shows the agent's state:
|
|
|
121
121
|
|
|
122
122
|
The page reads the state again every 15 seconds while an agent is queued, working or posting.
|
|
123
123
|
|
|
124
|
-
When the draft is ready, the case file
|
|
124
|
+
When the draft is ready, the case file shows it as a document, with its headings, lists and tables. Select **Open the draft** to read all of it. Then choose one option:
|
|
125
125
|
|
|
126
126
|
- **Post it to** the ticket posts the exact text. Nothing goes out until you hold to confirm.
|
|
127
127
|
- **Send it back with a note** returns the draft to the agent. Type what to change and press Enter.
|
|
128
128
|
- **Keep it as a draft** leaves the draft as it is.
|
|
129
|
+
- **Mark it done** closes the item. Nothing is posted.
|
|
130
|
+
|
|
131
|
+
To change the text yourself before you post it, select **Edit the draft**, change the text and select **Save the draft**. The case file then says **Edited by you**, and **Post it to** the ticket posts exactly your text.
|
|
129
132
|
|
|
130
133
|
The agent only drafts. It never posts to Jira until you approve the draft.
|
|
131
134
|
|
package/workspace/lib/day-run.js
CHANGED
|
@@ -249,6 +249,35 @@ function decide(run, taskId, op, now) {
|
|
|
249
249
|
});
|
|
250
250
|
}
|
|
251
251
|
|
|
252
|
+
/** The PM's own edit of a draft that waits for them (initiative c3-work-agent, amendment 13). The edited text replaces the
|
|
253
|
+
* body, so an approve posts exactly what the PM saved. The agent's first text stays in `agent_body`. */
|
|
254
|
+
function edit(run, taskId, body, now) {
|
|
255
|
+
const b = typeof body === 'string' ? body.trim() : '';
|
|
256
|
+
return withTask(run, taskId, (t) => {
|
|
257
|
+
if (t.status !== 'review' || !t.draft) return `task ${taskId} is ${t.status} — only a draft waiting for you can be edited`;
|
|
258
|
+
if (!b) return 'the draft cannot be empty — keep some text, or send it back with a note';
|
|
259
|
+
if (b.length > 8000) return 'the draft is over 8000 characters';
|
|
260
|
+
if (b === t.draft.body) return null;
|
|
261
|
+
if (t.draft.agent_body === undefined) t.draft.agent_body = t.draft.body;
|
|
262
|
+
t.draft.body = b; t.draft.edited_at = iso(now);
|
|
263
|
+
return null;
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** Approve a draft of an earlier day's run (amendment 13). The job CLI reads today's run only, so the approved task joins
|
|
268
|
+
* today's run as `sending`, with its draft as it is. `task` is the task of the earlier run. */
|
|
269
|
+
function carry(run, task, now) {
|
|
270
|
+
if (!run) return { ok: false, error: 'no day run today' };
|
|
271
|
+
if (!task || !TASK_RE.test(String(task.id || ''))) return { ok: false, error: 'no task to carry' };
|
|
272
|
+
if (task.status !== 'review' || !task.draft) return { ok: false, error: `task ${task.id} is ${task.status} — only a draft waiting for you can be approved` };
|
|
273
|
+
const next = clone(run);
|
|
274
|
+
if (findTask(next, task.id)) return { ok: false, error: `task ${task.id} is already in today's run` };
|
|
275
|
+
const t = clone(task);
|
|
276
|
+
Object.assign(t, { status: 'sending', session_id: null, prev_session: null, started_at: null, error: null, decided: { action: 'approve', at: iso(now) } });
|
|
277
|
+
next.tasks.push(t);
|
|
278
|
+
return { ok: true, run: next, task: task.id };
|
|
279
|
+
}
|
|
280
|
+
|
|
252
281
|
/** The send session reports the approved step done, with the link to what it posted. */
|
|
253
282
|
function sent(run, taskId, s, now) {
|
|
254
283
|
return withTask(run, taskId, (t) => {
|
|
@@ -413,5 +442,5 @@ function checkDone(run, { found = 0, resolved = 0 } = {}, now) {
|
|
|
413
442
|
}
|
|
414
443
|
|
|
415
444
|
module.exports = { MAX_PARALLEL, MAX_REVISIONS, MAX_CHECK_LOG, MAX_INTERVAL_MIN, STATUSES, KINDS, ACTIONS, TASK_RE,
|
|
416
|
-
newRun, readRun, findTask, modeOf, addTasks, addBriefed, addSend, toStart, started, deliver, decide, sent, fail, stop, withdraw, sessionEnded, sessionStalled, stage, intervalMin,
|
|
445
|
+
newRun, readRun, findTask, modeOf, addTasks, addBriefed, addSend, toStart, started, deliver, decide, edit, carry, sent, fail, stop, withdraw, sessionEnded, sessionStalled, stage, intervalMin,
|
|
417
446
|
checkDue, checkStarted, checkDone };
|
package/workspace/lib/my-work.js
CHANGED
|
@@ -194,7 +194,7 @@ function stateOf(item, presses, runs) {
|
|
|
194
194
|
if (!last || last.action === 'reopen') return { state: 'brief', sub: null, task: null, instruction: null };
|
|
195
195
|
if (last.action === 'handle') return { state: 'yours', sub: null, task: null, instruction: null, decided_at: last.at };
|
|
196
196
|
const t = taskOf(runs, last);
|
|
197
|
-
const task = t ? { id: t.id, status: t.status, error: t.error || null, draft: t.draft ? t.draft.summary : null, body: t.draft ? t.draft.body || null : null,
|
|
197
|
+
const task = t ? { id: t.id, status: t.status, error: t.error || null, draft: t.draft ? t.draft.summary : null, body: t.draft ? t.draft.body || null : null, edited_at: t.draft ? t.draft.edited_at || null : null,
|
|
198
198
|
sent: t.sent || null, started_at: t.started_at || null, revisions: Array.isArray(t.revisions) ? t.revisions.length : 0,
|
|
199
199
|
session: t.stalled_session || null, asks: t.asks || null } : null;
|
|
200
200
|
const base = { task, instruction: last.instruction, decided_at: last.at };
|
|
@@ -296,13 +296,19 @@ async function run(argv, { call, say, cwd = process.cwd() }) {
|
|
|
296
296
|
else {
|
|
297
297
|
// an item the PM added in Work (initiative c3-work-agent, amendment 11): the PM's item is the ask
|
|
298
298
|
const c = t.context || {};
|
|
299
|
-
say(`from Work: ${t.title}${c.due ? ' · due ' + c.due : ''}${c.from ? ' · with ' + c.from : ''}`);
|
|
299
|
+
say(`from Work: ${t.title}${c.due ? ' · due ' + c.due : ''}${c.from ? ' · with ' + c.from : ''}${t.link ? ' · link ' + t.link : ''}`);
|
|
300
|
+
for (const k of ['where', 'about', 'ask', 'how', 'why_now']) if (c[k] && c[k] !== t.title) say(`${k.replace('_', ' ')}: ${c[k]}`);
|
|
300
301
|
}
|
|
301
302
|
// the PM's ask, as Work or Your day gave it, including a changed ask (c3-work-agent amendments 10 and 11)
|
|
302
303
|
if (t.instruction && !t.my_work) say(`instruction from the PM (this is the ask): ${t.instruction}`);
|
|
303
304
|
if (t.kind === 'approve') say('kind approve: deliver ONE draft; nothing goes out until the PM approves it.');
|
|
304
305
|
else say('kind auto: deliver ONE draft; nothing leaves this machine.');
|
|
305
|
-
if (t.draft) {
|
|
306
|
+
if (t.draft) {
|
|
307
|
+
say(`draft (${t.draft.at}): ${t.draft.summary}${t.draft.link ? ' · ' + t.draft.link : ''}`);
|
|
308
|
+
// the PM edited the draft (c3-work-agent amendment 13): the body below is the PM's text, to post as it is
|
|
309
|
+
if (t.draft.edited_at) say(`edited by the PM (${t.draft.edited_at}): the text below is the PM's own. Post it exactly as written.`);
|
|
310
|
+
say(t.draft.body);
|
|
311
|
+
}
|
|
306
312
|
t.revisions.forEach((rv, i) => say(`revision note ${i + 1} (${rv.at}): ${rv.note}`));
|
|
307
313
|
if (t.decided) say(`decided: ${t.decided.action} at ${t.decided.at}${t.decided.note ? ' — ' + t.decided.note : ''}`);
|
|
308
314
|
if (t.sent) say(`sent ${t.sent.at}${t.sent.link ? ' · ' + t.sent.link : ''}`);
|
|
@@ -7,9 +7,10 @@
|
|
|
7
7
|
* POST /api/my-work/act { action: brief|handle|reopen, item, instruction?, ws }
|
|
8
8
|
* → { ok, item, task? } the PM's press; a brief adds one day task (R5)
|
|
9
9
|
* POST /api/my-work/refresh { ws } → { ok, session } Read again: starts the my-work-read skill
|
|
10
|
-
* POST /api/my-work/decide { item, action: approve|revise|retry, note? }
|
|
10
|
+
* POST /api/my-work/decide { item, action: approve|revise|retry|edit, note?, body? }
|
|
11
11
|
* → { ok, item, task } the PM's decision on the agent's draft (initiative c3-work-agent):
|
|
12
|
-
* approve posts exactly the draft, revise sends it back with the note, retry runs a failed task
|
|
12
|
+
* approve posts exactly the draft, revise sends it back with the note, retry runs a failed task
|
|
13
|
+
* again, edit saves the PM's own text as the draft (approve with body: edit, then approve)
|
|
13
14
|
* POST /api/my-work/stop { item } → { ok, item } ends the item's working or queued task and offers the suggestion again
|
|
14
15
|
*
|
|
15
16
|
* STATE. The latest read is <runtime>/my-work/read.json and the presses are <runtime>/my-work/actions.jsonl, append-only.
|
|
@@ -156,11 +157,13 @@ const nextDay = (d) => new Date(Date.parse(d + 'T00:00:00Z') + DAY_MS).toISOStri
|
|
|
156
157
|
/** The day run that holds the task of the item's last brief: the brief's day (UTC, as the press records it) up to today. */
|
|
157
158
|
function runOfTask(ctx, brief, today) {
|
|
158
159
|
const id = myWork.taskSpec({ id: brief.item }, brief).id;
|
|
160
|
+
// the newest: an approved draft of an earlier day joins today's run (amendment 13)
|
|
161
|
+
let found = null;
|
|
159
162
|
for (let d = String(brief.at).slice(0, 10), n = 0; d <= today && n < 400; d = nextDay(d), n++) {
|
|
160
163
|
let run = null; try { run = ctx.readDayRun ? ctx.readDayRun(d) : null; } catch { run = null; }
|
|
161
|
-
if (run && dayRun.findTask(run, id))
|
|
164
|
+
if (run && dayRun.findTask(run, id)) found = { run, id };
|
|
162
165
|
}
|
|
163
|
-
return
|
|
166
|
+
return found;
|
|
164
167
|
}
|
|
165
168
|
|
|
166
169
|
/** decide (approve | revise | retry) or stop, on the task of the item's last brief. */
|
|
@@ -175,12 +178,10 @@ function taskPress(ctx, res, verb, body, now) {
|
|
|
175
178
|
const found = runOfTask(ctx, brief, list.today);
|
|
176
179
|
if (verb === 'decide') {
|
|
177
180
|
if (!found) { ctx.json(res, { ok: false, error: 'the agent has not started this item yet' }, 409); return; }
|
|
178
|
-
const
|
|
179
|
-
if (!
|
|
180
|
-
const run = ctx.startDayTasks ? ctx.startDayTasks(r.run, now) : r.run;
|
|
181
|
-
if (!ctx.writeDayRun || !ctx.writeDayRun(run)) { ctx.json(res, { ok: false, error: 'could not save — nothing changed' }, 500); return; }
|
|
181
|
+
const d = decideDraft(ctx, found.run, found.id, body, now);
|
|
182
|
+
if (!d.ok) { ctx.json(res, { ok: false, error: d.error }, d.status || 400); return; }
|
|
182
183
|
const after = listFor(ctx, Date.now()).items.find((x) => x.id === item) || null;
|
|
183
|
-
ctx.json(res, { ok: true, item: after, task: dayRun.findTask(run, found.id) });
|
|
184
|
+
ctx.json(res, { ok: true, item: after, task: dayRun.findTask(d.run, found.id) });
|
|
184
185
|
return;
|
|
185
186
|
}
|
|
186
187
|
// stop: only a task that has not delivered yet; its session ends and the item offers the suggestion again
|
|
@@ -200,6 +201,32 @@ function taskPress(ctx, res, verb, body, now) {
|
|
|
200
201
|
ctx.json(res, { ok: true, item: after });
|
|
201
202
|
}
|
|
202
203
|
|
|
204
|
+
/**
|
|
205
|
+
* The PM's decision on a draft in `run` (amendment 13): edit saves the PM's text as the draft; approve posts the draft,
|
|
206
|
+
* with the PM's text first when the press carries `body`; revise and retry as lib/day-run.js decide. A draft of an earlier
|
|
207
|
+
* day is approved in today's run, because the job CLI reads today's run only. Returns { ok, run } (the run that holds the
|
|
208
|
+
* task now) or { ok: false, error, status }.
|
|
209
|
+
*/
|
|
210
|
+
function decideDraft(ctx, run, taskId, body, now) {
|
|
211
|
+
const save = (r) => (ctx.writeDayRun && ctx.writeDayRun(r) ? { ok: true, run: r } : { ok: false, status: 500, error: 'could not save — nothing changed' });
|
|
212
|
+
let cur = run;
|
|
213
|
+
if (body.action === 'edit' || (body.action === 'approve' && typeof body.body === 'string')) {
|
|
214
|
+
const e = dayRun.edit(cur, taskId, body.body, now);
|
|
215
|
+
if (!e.ok) return { ok: false, error: e.error };
|
|
216
|
+
if (body.action === 'edit') return save(e.run);
|
|
217
|
+
cur = e.run;
|
|
218
|
+
}
|
|
219
|
+
if (body.action === 'approve' && cur.date !== localDate(now)) {
|
|
220
|
+
const t = dayRun.findTask(cur, taskId);
|
|
221
|
+
if (cur !== run) { const w = save(cur); if (!w.ok) return w; }
|
|
222
|
+
const b = ctx.myWorkBrief(t, String(body.ws || cur.ws || ''), (today, spec, at) => dayRun.carry(today, spec, at));
|
|
223
|
+
return b.ok ? { ok: true, run: b.run } : { ok: false, status: b.status || 500, error: b.error };
|
|
224
|
+
}
|
|
225
|
+
const r = dayRun.decide(cur, taskId, { action: body.action, note: body.note }, now);
|
|
226
|
+
if (!r.ok) return { ok: false, error: r.error };
|
|
227
|
+
return save(ctx.startDayTasks ? ctx.startDayTasks(r.run, now) : r.run);
|
|
228
|
+
}
|
|
229
|
+
|
|
203
230
|
/* ── the agent on an item the morning brief suggests for the PM (c3-work-agent amendment 10) and on an item the PM added
|
|
204
231
|
* in Work (amendment 11). Item `brief:<id>`: its task is `t-<id>` in today's day run, added as Your day's "Have the agent
|
|
205
232
|
* draft it" adds it. Item `item-<hex>`: its task is `t-<hex>`, in the newest day run of the last 7 days that holds it. */
|
|
@@ -208,13 +235,13 @@ const LOCAL_ITEM = /^item-[0-9a-f]{12}$/;
|
|
|
208
235
|
|
|
209
236
|
/** The item the press names, as the agent sees it: its task id, the offer, the spec a press adds and its agent field. */
|
|
210
237
|
function targetOf(ctx, itemId, now) {
|
|
211
|
-
const today = localDate(now);
|
|
212
238
|
const bm = BRIEF_ITEM.exec(itemId);
|
|
213
239
|
if (bm) {
|
|
214
|
-
|
|
215
|
-
const
|
|
216
|
-
|
|
217
|
-
|
|
240
|
+
// today's brief, or an earlier one of the last 7 days when the PM kept the item in Work (amendment 12)
|
|
241
|
+
const readBriefOf = (d) => { try { return JSON.parse(fs.readFileSync(path.join(ctx.runtimeDir, 'briefs', d + '.json'), 'utf8')); } catch { return null; } };
|
|
242
|
+
const x = assigned.findBriefItem(readBriefOf, bm[1], now);
|
|
243
|
+
if (!x) return { error: 'no item of yours with that id in a brief of the last 7 days' };
|
|
244
|
+
const run = assigned.runWith(assigned.recentRuns(ctx.readDayRun, now), 't-' + x.id);
|
|
218
245
|
return { postable: true, taskId: 't-' + x.id, suggest: assigned.briefSuggest(x), spec: (own) => assigned.briefDraftSpec(x, own), run, agent: (r) => assigned.briefAgent(x, r) };
|
|
219
246
|
}
|
|
220
247
|
// an item the PM added in Work (amendment 11): its draft is kept across days, so the newest run that holds its task decides
|
|
@@ -257,9 +284,9 @@ function agentItemPress(ctx, res, verb, body, itemId, now) {
|
|
|
257
284
|
after(b.run, b.task); return;
|
|
258
285
|
}
|
|
259
286
|
if (verb === 'decide') {
|
|
260
|
-
const
|
|
261
|
-
if (!
|
|
262
|
-
|
|
287
|
+
const d = decideDraft(ctx, run, taskId, body, now);
|
|
288
|
+
if (!d.ok) { ctx.json(res, { ok: false, error: d.error }, d.status || 400); return; }
|
|
289
|
+
after(d.run, dayRun.findTask(d.run, taskId)); return;
|
|
263
290
|
} else {
|
|
264
291
|
// stop: only a task that has not delivered; its session ends and the task leaves the run, so the item offers the draft again
|
|
265
292
|
if (t.status !== 'waiting' && t.status !== 'working' && t.status !== 'failed') { ctx.json(res, { ok: false, error: `the agent's task is ${t.status} — there is nothing to stop` }, 409); return; }
|
|
@@ -349,6 +349,14 @@ module.exports = {
|
|
|
349
349
|
// an item the PM added: the agent can draft a first version of it (c3-work-agent amendment 11)
|
|
350
350
|
const runs = assigned.recentRuns(ctx.readDayRun, c.now.getTime());
|
|
351
351
|
for (const it of v.items) if (!it.agent && assigned.localOffer(it)) it.agent = assigned.localAgent(it, assigned.runWith(runs, assigned.localTaskId(it)));
|
|
352
|
+
// an item of an earlier brief the PM kept in Work keeps its offer (amendment 12): it is no longer in today's brief
|
|
353
|
+
const readBriefOf = (d) => { try { return JSON.parse(fs.readFileSync(path.join(ctx.runtimeDir, 'briefs', d + '.json'), 'utf8')); } catch { return null; } };
|
|
354
|
+
for (const it of v.items) {
|
|
355
|
+
const m = /^brief:([0-9a-f]{12})$/.exec(String(it.id || ''));
|
|
356
|
+
if (!m || it.agent || it.done || !ctx.runtimeDir) continue;
|
|
357
|
+
const x = assigned.findBriefItem(readBriefOf, m[1], c.now.getTime());
|
|
358
|
+
if (x) { it.agent = assigned.briefAgent(x, assigned.runWith(runs, 't-' + x.id)); if (!it.place) it.place = x.source || null; }
|
|
359
|
+
}
|
|
352
360
|
ctx.json(res, v);
|
|
353
361
|
return true;
|
|
354
362
|
}
|