create-pmos 0.55.2 → 0.55.4
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/guide/today.md +4 -0
- package/workspace/lib/day-run.js +9 -3
- package/workspace/lib/first-input.js +89 -0
- package/workspace/package-lock.json +1 -1
- package/workspace/package.json +1 -1
- package/workspace/public/pm/dest-today.js +6 -2
- package/workspace/public/pm/today-copy.js +5 -1
- package/workspace/public/pm/today-day.js +34 -2
- package/workspace/server.js +39 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-pmos",
|
|
3
|
-
"version": "0.55.
|
|
3
|
+
"version": "0.55.4",
|
|
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-06T09:30:22Z"}
|
|
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-06T09:30:22Z"}
|
|
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-06T09:30:22Z"}
|
|
6
6
|
status: stable
|
|
7
7
|
---
|
|
8
8
|
|
package/workspace/guide/today.md
CHANGED
|
@@ -18,6 +18,10 @@ Press **Prepare my day** to start the day. It runs from the brief to the deliver
|
|
|
18
18
|
|
|
19
19
|
If the morning-brief session stops before it submits a brief, the Read step says so and gives the reason. **Prepare my day** is then ready again, so press it to try once more. If the session says that its folder is not there, open the repository again from its current folder first.
|
|
20
20
|
|
|
21
|
+
Each agent job runs in its own session, so you can open any job and follow or steer it. The workspace types the job's prompt into the session when the agent is ready to read, and presses Enter for you. When you approve or revise a job and its session is still open, the workspace uses that same session again. Prepare again works the same way: when the morning-brief session is still open and has finished its read, or has printed nothing for two minutes, the workspace types the read into that session and runs it again.
|
|
22
|
+
|
|
23
|
+
**Agent results**, at the top of the left column, counts the jobs and lists each finished one with its draft summary or the reason it failed. The other things that wait for you, such as outcome checks, report drafts and sessions, are in **Also waiting for you** at the end of the left column. When the day is folded, they show under it.
|
|
24
|
+
|
|
21
25
|
Each job has one of three kinds.
|
|
22
26
|
|
|
23
27
|
| Kind | Who does it | What you do |
|
package/workspace/lib/day-run.js
CHANGED
|
@@ -149,6 +149,12 @@ function deliver(run, taskId, d, now) {
|
|
|
149
149
|
});
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
+
/** The task leaves its session: `prev_session` keeps its id, so the next step can run in the same session while it is open. */
|
|
153
|
+
function keepSession(t) {
|
|
154
|
+
if (t.session_id) t.prev_session = t.session_id;
|
|
155
|
+
t.session_id = null;
|
|
156
|
+
}
|
|
157
|
+
|
|
152
158
|
/**
|
|
153
159
|
* The PM's decision on a draft.
|
|
154
160
|
* approve review (approve task) → sending: a session will do exactly the approved step. done (auto task) → recorded.
|
|
@@ -162,7 +168,7 @@ function decide(run, taskId, op, now) {
|
|
|
162
168
|
return withTask(run, taskId, (t) => {
|
|
163
169
|
const at = iso(now);
|
|
164
170
|
if (o.action === 'approve') {
|
|
165
|
-
if (t.status === 'review' && t.kind === 'approve') { t.status = 'sending'; t
|
|
171
|
+
if (t.status === 'review' && t.kind === 'approve') { t.status = 'sending'; keepSession(t); t.started_at = null; }
|
|
166
172
|
else if (!(t.status === 'done' && t.kind === 'auto')) return `task ${taskId} is ${t.status} — only a draft waiting for you can be approved`;
|
|
167
173
|
t.decided = { action: 'approve', at, ...(note ? { note: note.slice(0, 500) } : {}) };
|
|
168
174
|
return null;
|
|
@@ -173,11 +179,11 @@ function decide(run, taskId, op, now) {
|
|
|
173
179
|
if (note.length > 500) return 'the note is over 500 characters';
|
|
174
180
|
if (t.revisions.length >= MAX_REVISIONS) return `task ${taskId} has had ${MAX_REVISIONS} revisions — at most ${MAX_REVISIONS}. Do the rest yourself.`;
|
|
175
181
|
t.revisions.push({ note, at });
|
|
176
|
-
t.status = 'waiting'; t
|
|
182
|
+
t.status = 'waiting'; keepSession(t); t.decided = { action: 'revise', at, note };
|
|
177
183
|
return null;
|
|
178
184
|
}
|
|
179
185
|
if (t.status !== 'failed') return `task ${taskId} is ${t.status} — only a failed task can be run again`;
|
|
180
|
-
t.status = 'waiting'; t
|
|
186
|
+
t.status = 'waiting'; keepSession(t); t.error = null; t.decided = { action: 'retry', at };
|
|
181
187
|
return null;
|
|
182
188
|
});
|
|
183
189
|
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/* first-input.js — how the workspace types a session's first input, the prompt it was started for.
|
|
3
|
+
*
|
|
4
|
+
* The old way wrote the whole prompt and its Enter as one burst, 700 ms after the start. An agent CLI such as Claude Code
|
|
5
|
+
* is not ready to read at 700 ms: it is still starting, it may ask its own startup questions, and it reads a fast burst of
|
|
6
|
+
* text as a paste. So the prompt was lost or sat in the input box without Enter, and the PM opened an empty session.
|
|
7
|
+
*
|
|
8
|
+
* Now the workspace waits until the agent is ready, then types the prompt, then presses Enter as a separate key:
|
|
9
|
+
* 1. READY: the pane printed something and then was quiet for QUIET_MS, and at least MIN_MS passed. A program that prints
|
|
10
|
+
* nothing (a plain reader of standard input) is ready after SILENT_MS. After MAX_MS it is typed in any case.
|
|
11
|
+
* 2. TYPE: when the program turned on bracketed paste (it printed ESC[?2004h), the prompt is sent as one paste, so its
|
|
12
|
+
* lines stay one prompt. Otherwise it is sent as typed text.
|
|
13
|
+
* 3. ENTER: SUBMIT_MS later, Enter alone. A bracketed-paste program that printed nothing after that Enter gets Enter once
|
|
14
|
+
* more after RETRY_MS, because the first one can land while the paste is still being read.
|
|
15
|
+
* The clock and the timers are injected, so the plan is tested without waiting. No I/O of its own: it writes only through
|
|
16
|
+
* `write`.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const MIN_MS = 1200;
|
|
20
|
+
const QUIET_MS = 800;
|
|
21
|
+
const SILENT_MS = 3000;
|
|
22
|
+
const MAX_MS = 20000;
|
|
23
|
+
const POLL_MS = 200;
|
|
24
|
+
const SUBMIT_MS = 500;
|
|
25
|
+
const RETRY_MS = 1500;
|
|
26
|
+
const PASTE_ON = '\x1b[?2004h';
|
|
27
|
+
const PASTE_OFF = '\x1b[?2004l';
|
|
28
|
+
|
|
29
|
+
/** True when the last bracketed-paste switch in `buf` turned it on. */
|
|
30
|
+
function pasteOn(buf) {
|
|
31
|
+
const b = String(buf || '');
|
|
32
|
+
return b.lastIndexOf(PASTE_ON) > b.lastIndexOf(PASTE_OFF);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Is the pane ready for typed input? `t0` is when the wait began; `firstOut` is when output first came after t0. */
|
|
36
|
+
function ready({ now, t0, lastOutputAt, firstOut }) {
|
|
37
|
+
const waited = now - t0;
|
|
38
|
+
if (waited >= MAX_MS) return true;
|
|
39
|
+
if (waited < MIN_MS) return false;
|
|
40
|
+
if (!firstOut) return waited >= SILENT_MS;
|
|
41
|
+
return now - lastOutputAt >= QUIET_MS;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Split a first input into the text and whether it ended with Enter. */
|
|
45
|
+
function splitInput(input) {
|
|
46
|
+
const s = String(input == null ? '' : input);
|
|
47
|
+
const m = /[\r\n]+$/.exec(s);
|
|
48
|
+
return { text: m ? s.slice(0, m.index) : s, enter: !!m };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Type `input` into session `s` once it is ready. `s` is read live (s.pty, s.buf, s.lastOutputAt, s.state).
|
|
53
|
+
* @param {object} s the session record
|
|
54
|
+
* @param {string} input the prompt, with a trailing Enter when it must be submitted
|
|
55
|
+
* @param {object} [o] { now, setTimeout, onDone } — injected for tests
|
|
56
|
+
*/
|
|
57
|
+
function deliver(s, input, o = {}) {
|
|
58
|
+
const now = o.now || (() => Date.now());
|
|
59
|
+
const later = o.setTimeout || setTimeout;
|
|
60
|
+
const done = typeof o.onDone === 'function' ? o.onDone : () => {};
|
|
61
|
+
const { text, enter } = splitInput(input);
|
|
62
|
+
if (!s || (!text && !enter)) { done('nothing'); return; }
|
|
63
|
+
const t0 = now();
|
|
64
|
+
const base = s.lastOutputAt || 0;
|
|
65
|
+
let firstOut = 0;
|
|
66
|
+
const alive = () => s.pty && s.state !== 'exited';
|
|
67
|
+
const write = (d) => { try { s.pty.write(d); return true; } catch { return false; } };
|
|
68
|
+
const tick = () => {
|
|
69
|
+
if (!alive()) { done('gone'); return; }
|
|
70
|
+
if (!firstOut && (s.lastOutputAt || 0) > base) firstOut = s.lastOutputAt;
|
|
71
|
+
if (!ready({ now: now(), t0, lastOutputAt: s.lastOutputAt || 0, firstOut })) { later(tick, POLL_MS); return; }
|
|
72
|
+
const paste = pasteOn(s.buf);
|
|
73
|
+
if (text) write(paste ? '\x1b[200~' + text + '\x1b[201~' : text);
|
|
74
|
+
if (!enter) { done('typed'); return; }
|
|
75
|
+
later(() => {
|
|
76
|
+
if (!alive()) { done('gone'); return; }
|
|
77
|
+
const before = s.lastOutputAt || 0;
|
|
78
|
+
write('\r');
|
|
79
|
+
if (!paste) { done('submitted'); return; }
|
|
80
|
+
later(() => {
|
|
81
|
+
if (alive() && (s.lastOutputAt || 0) <= before) { write('\r'); done('resubmitted'); return; }
|
|
82
|
+
done('submitted');
|
|
83
|
+
}, RETRY_MS);
|
|
84
|
+
}, text ? SUBMIT_MS : 0);
|
|
85
|
+
};
|
|
86
|
+
later(tick, POLL_MS);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
module.exports = { deliver, ready, pasteOn, splitInput, MIN_MS, QUIET_MS, SILENT_MS, MAX_MS, SUBMIT_MS, RETRY_MS };
|
package/workspace/package.json
CHANGED
|
@@ -122,8 +122,12 @@ function build(ctx0) {
|
|
|
122
122
|
// joined to the cards of the first repository in scope, and its initiatives open in that repository
|
|
123
123
|
const dayRepo = sc.repos.length ? sc.repos[0] : null;
|
|
124
124
|
const dayWs = dayRepo ? wsOf(dayRepo) : null;
|
|
125
|
-
|
|
126
|
-
|
|
125
|
+
// the open day takes the list into its left column ("Also waiting for you"), so Today shows one layout; a folded day
|
|
126
|
+
// leaves it below, as before
|
|
127
|
+
const owed = { list, more, count: n };
|
|
128
|
+
const day = daySection({ ws: dayWs, setState: ctx0.setState || setState, openIni: dayRepo ? (id) => openInitiative(dayRepo, id) : null, owed });
|
|
129
|
+
const inDay = !!(day && day.contains(list));
|
|
130
|
+
return { children: inDay ? [head, day, health] : [head, day, health, list, more], sig: JSON.stringify([sc.all, sc.repo, n, ready, expanded, banner]) + daySig(dayWs) + nodes.map((x) => x.outerHTML).join('') };
|
|
127
131
|
}
|
|
128
132
|
|
|
129
133
|
/** The key of the control that has the focus inside the board, so it can be focused again after a rebuild. */
|
|
@@ -128,7 +128,11 @@ export const COPY = {
|
|
|
128
128
|
unreached: 'The workspace did not answer.',
|
|
129
129
|
refused: (why) => `Nothing changed. ${why}`,
|
|
130
130
|
oldServer: 'This workspace server has no day run yet. Only your goals show.',
|
|
131
|
-
panels: { calendar: 'Calendar', jobs: 'Jobs', goals: 'Goals for today', mentions: 'Mentions since yesterday', detail: 'Detail' },
|
|
131
|
+
panels: { summary: 'Agent results', calendar: 'Calendar', jobs: 'Jobs', goals: 'Goals for today', mentions: 'Mentions since yesterday', waiting: 'Also waiting for you', detail: 'Detail' },
|
|
132
|
+
summary: (c) => [c.review ? `${c.review} ${c.review === 1 ? 'draft waits' : 'drafts wait'} for you` : null, c.done ? `${c.done} done` : null,
|
|
133
|
+
c.working ? `${c.working} working` : null, c.failed ? `${c.failed} failed` : null].filter(Boolean).join(' · ') || 'No agent job yet.',
|
|
134
|
+
summaryAll: (n) => `All ${n} agent ${n === 1 ? 'job has' : 'jobs have'} a result.`,
|
|
135
|
+
waitingHint: 'Outcome checks, reports and sessions from your repositories, outside today\'s jobs.',
|
|
132
136
|
now: (at) => `now ${at}`,
|
|
133
137
|
noCalendar: 'No events read for today.',
|
|
134
138
|
noJobs: 'No jobs yet. Press Prepare my day. Each job shows here; select one to see what it needs.',
|
|
@@ -378,6 +378,38 @@ function jobsPanel(ctx) {
|
|
|
378
378
|
el('ul', { class: 'td-day__rows' }, ...l.map((j) => jobRow(ctx, j))))));
|
|
379
379
|
}
|
|
380
380
|
|
|
381
|
+
/* The coordinator's view of the run: what every agent job came back with, in one place. The server runs the jobs as one
|
|
382
|
+
* session each and keeps their state; this reads that state, so it needs no session of its own. */
|
|
383
|
+
function summaryPanel(ctx) {
|
|
384
|
+
const { m } = ctx;
|
|
385
|
+
const ts = m.tasks;
|
|
386
|
+
if (!ts.length) return null;
|
|
387
|
+
const c = { review: 0, done: 0, working: 0, failed: 0 };
|
|
388
|
+
for (const t of ts) {
|
|
389
|
+
if (t.status === 'review') c.review++;
|
|
390
|
+
else if (t.status === 'done' || t.status === 'sent') c.done++;
|
|
391
|
+
else if (t.status === 'failed') c.failed++;
|
|
392
|
+
else c.working++;
|
|
393
|
+
}
|
|
394
|
+
const finished = ts.filter((t) => t.status !== 'waiting' && t.status !== 'working' && t.status !== 'sending');
|
|
395
|
+
const byTask = new Map(m.jobs.filter((j) => j.task).map((j) => [j.task.id, j]));
|
|
396
|
+
const rows = finished.map((t) => {
|
|
397
|
+
const j = byTask.get(t.id);
|
|
398
|
+
const what = t.status === 'failed' ? t.error : t.sent && t.sent.link ? t.sent.link : t.draft ? t.draft.summary : '';
|
|
399
|
+
return j ? rowBtn(ctx, 'job', j.id, { lead: '·', title: j.title, meta: what || '', trail: statusPill(j) }) : null;
|
|
400
|
+
}).filter(Boolean);
|
|
401
|
+
return panel('summary', null,
|
|
402
|
+
el('p', { class: 'td-day__note', 'data-day': 'summary' }, c.working ? D.summary(c) : D.summaryAll(ts.length) + ' ' + D.summary(c)),
|
|
403
|
+
rows.length ? el('ul', { class: 'td-day__rows', 'data-day-list': 'summary' }, ...rows) : null);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/* What waits for the PM outside today's jobs (the Today cards: outcome checks, reports, sessions). Today hands its list
|
|
407
|
+
* in, so the cards keep their own controls and the open day shows one layout. */
|
|
408
|
+
function waitingPanel(owed) {
|
|
409
|
+
if (!owed || !owed.list) return null;
|
|
410
|
+
return panel('waiting', owed.count ? String(owed.count) : null, note(D.waitingHint), owed.list, owed.more || null);
|
|
411
|
+
}
|
|
412
|
+
|
|
381
413
|
function goalsPanel(ctx) {
|
|
382
414
|
const { m } = ctx;
|
|
383
415
|
return panel('goals', D.goalsCount(m.goals.filter((g) => g.status === 'open').length, m.maxGoals), goalRows(ctx), goalAdd(ctx));
|
|
@@ -721,7 +753,7 @@ function fold(open) {
|
|
|
721
753
|
}
|
|
722
754
|
|
|
723
755
|
/** The section. `ws` is the repository the day run belongs to; `openIni(id)` opens an initiative of it. */
|
|
724
|
-
export function daySection({ ws, setState, openIni } = {}) {
|
|
756
|
+
export function daySection({ ws, setState, openIni, owed } = {}) {
|
|
725
757
|
if (FIXTURE) return null;
|
|
726
758
|
// like Today's own feeds, a read that lands re-renders only while the section is on the page: a hidden Today must not
|
|
727
759
|
// cause a whole-page render under whatever the PM is doing elsewhere
|
|
@@ -755,7 +787,7 @@ export function daySection({ ws, setState, openIni } = {}) {
|
|
|
755
787
|
c.error ? el('p', { class: 'td-day__run td-day__run--failed', role: 'status', 'data-day': 'error' }, D.refused(c.error)) : null,
|
|
756
788
|
m.d && m.d.legacy ? note(D.oldServer) : null,
|
|
757
789
|
el('div', { class: 'td-day__cols' },
|
|
758
|
-
el('div', { class: 'td-day__left' }, calendar(ctx), jobsPanel(ctx), goalsPanel(ctx), mentionsPanel(ctx)),
|
|
790
|
+
el('div', { class: 'td-day__left' }, summaryPanel(ctx), calendar(ctx), jobsPanel(ctx), goalsPanel(ctx), mentionsPanel(ctx), waitingPanel(owed)),
|
|
759
791
|
detail(ctx)));
|
|
760
792
|
}
|
|
761
793
|
|
package/workspace/server.js
CHANGED
|
@@ -85,6 +85,7 @@ const repoWatch = require('./lib/repo-watch.js');
|
|
|
85
85
|
tsSettings.adoptEnv(process.env); // D79 — take the env key into memory and delete it, before anything is spawned
|
|
86
86
|
const scheduleLogic = require('./lib/schedule-logic');
|
|
87
87
|
const { safeFirstInput } = require('./lib/prompt-input');
|
|
88
|
+
const firstInput = require('./lib/first-input');
|
|
88
89
|
const transcriptsLib = require('./lib/transcripts');
|
|
89
90
|
const initiativeFiles = require('./lib/initiative-files');
|
|
90
91
|
const { deriveNextSteps } = require('./lib/next-steps');
|
|
@@ -3135,7 +3136,9 @@ function spawnSession({ name, agent, cwd, ws, origin, initialInput, profile, ini
|
|
|
3135
3136
|
// a scheduled run tags the session with its origin, and feeds the skill invocation as first input
|
|
3136
3137
|
const tag = origin || null;
|
|
3137
3138
|
// safeFirstInput: a first input that begins with `/` is read by the agent as a slash command, not as the task.
|
|
3138
|
-
|
|
3139
|
+
// firstInput.deliver waits until the agent is ready to read, types the prompt, then presses Enter as its own key
|
|
3140
|
+
// (lib/first-input.js): a burst written at a fixed 700 ms was lost or left unsent while the agent was still starting.
|
|
3141
|
+
const feed = (s) => { if (initialInput && s && s.pty) { try { firstInput.deliver(s, safeFirstInput(initialInput)); } catch {} } };
|
|
3139
3142
|
// shell gets a real fallback to `sh`; agent CLIs run as-is (harness untouched), NO fallback —
|
|
3140
3143
|
// falling back from `claude` to `sh` would be a lie.
|
|
3141
3144
|
const cmd = isShell ? (commandExists(SHELL) ? SHELL : 'sh') : agent;
|
|
@@ -3636,6 +3639,18 @@ function briefOfDay(date) {
|
|
|
3636
3639
|
try { return JSON.parse(fs.readFileSync(path.join(STATE, 'briefs', date + '.json'), 'utf8')); } catch { return null; }
|
|
3637
3640
|
}
|
|
3638
3641
|
const daySessionAlive = (id) => { const s = id && sessions.get(id); return !!s && s.state !== 'exited'; };
|
|
3642
|
+
// An agent CLI does not exit when its work is done: the morning-brief session stays open after it submits the brief. So
|
|
3643
|
+
// "alive" alone does not mean "still reading". The read is over when today's brief arrived after the session started,
|
|
3644
|
+
// or when the pane has printed nothing for BRIEF_IDLE_MS without a brief (it waits on a question, or never got its prompt).
|
|
3645
|
+
const BRIEF_IDLE_MS = 2 * 60 * 1000;
|
|
3646
|
+
function briefSessionDone(run, date, now) {
|
|
3647
|
+
const bs = run && run.brief_session; const s = bs && sessions.get(bs.id);
|
|
3648
|
+
if (!s || s.state === 'exited' || bs.ended_at) return null;
|
|
3649
|
+
let at = 0; try { at = fs.statSync(path.join(STATE, 'briefs', date + '.json')).mtimeMs; } catch { /* no brief yet */ }
|
|
3650
|
+
if (at && at >= Date.parse(bs.started_at || 0)) return 'submitted';
|
|
3651
|
+
if (now - Math.max(s.lastOutputAt || 0, Date.parse(bs.started_at || 0) || 0) >= BRIEF_IDLE_MS) return 'idle';
|
|
3652
|
+
return null;
|
|
3653
|
+
}
|
|
3639
3654
|
// The skill's own body is the prompt, as fireSchedule hands it over; the line after it names this run's task and mode.
|
|
3640
3655
|
function daySkillPrompt(run, skillId) {
|
|
3641
3656
|
const w = getWs(run.ws);
|
|
@@ -3647,8 +3662,15 @@ function daySkillPrompt(run, skillId) {
|
|
|
3647
3662
|
function startDayTask(run, t, now) {
|
|
3648
3663
|
const sk = daySkillPrompt(run, 'day-task');
|
|
3649
3664
|
let s = null;
|
|
3650
|
-
|
|
3651
|
-
|
|
3665
|
+
const line = `This run: /day-task ${t.id} ${t.mode} — read the task first with: pmos proactive day show ${t.id}`;
|
|
3666
|
+
// The session that did the task's last step is used again while it is open: it has the skill and the task in its
|
|
3667
|
+
// context already, so only this run's line is typed into it, and the PM keeps one session per job.
|
|
3668
|
+
const prev = t.prev_session && sessions.get(t.prev_session);
|
|
3669
|
+
if (!sk.error && prev && prev.state !== 'exited' && prev.pty) {
|
|
3670
|
+
try { firstInput.deliver(prev, line + '\r'); s = prev; } catch { s = null; }
|
|
3671
|
+
}
|
|
3672
|
+
if (!sk.error && !s) {
|
|
3673
|
+
const prompt = `${sk.prompt}\n\n${line}`;
|
|
3652
3674
|
s = spawnSession({ name: `${t.title} · day task`, agent: sk.task.agent || 'claude', ws: sk.w ? sk.w.id : run.ws, origin: 'day-task', initialInput: prompt + '\r',
|
|
3653
3675
|
modelAsk: { scheduleTier: null, skillTier: sk.task.tier || null, skill: sk.task.skill } });
|
|
3654
3676
|
}
|
|
@@ -7168,6 +7190,18 @@ async function handleRequest(req, res) {
|
|
|
7168
7190
|
let run = readDayRun(pdate) || dayRunLib.newRun(pdate, pnow);
|
|
7169
7191
|
run.ws = w.id;
|
|
7170
7192
|
if (run.brief_session && !run.brief_session.ended_at) {
|
|
7193
|
+
// A session that finished its read (or sits idle) is used again: the skill's prompt is typed into it and run.
|
|
7194
|
+
const done = daySessionAlive(run.brief_session.id) ? briefSessionDone(run, pdate, pnow) : null;
|
|
7195
|
+
if (done) {
|
|
7196
|
+
const sk = daySkillPrompt(run, 'morning-brief');
|
|
7197
|
+
const s = sessions.get(run.brief_session.id);
|
|
7198
|
+
if (!sk.error) {
|
|
7199
|
+
try { firstInput.deliver(s, `${sk.prompt}\n\nThis run: prepare the day again. Read every source again and submit a new brief for today.\r`); } catch { /* typed below as a new session */ }
|
|
7200
|
+
run.brief_session = { id: s.id, started_at: new Date(pnow).toISOString(), ended_at: null };
|
|
7201
|
+
writeDayRun(run);
|
|
7202
|
+
return json(res, { ok: true, started: true, reused: true, session: s.id, run, goals: open.length });
|
|
7203
|
+
}
|
|
7204
|
+
}
|
|
7171
7205
|
if (daySessionAlive(run.brief_session.id)) { writeDayRun(run); return json(res, { ok: true, running: true, session: run.brief_session.id, run, goals: open.length }); }
|
|
7172
7206
|
run = dayRunLib.sessionEnded(run, run.brief_session.id, pnow).run;
|
|
7173
7207
|
}
|
|
@@ -7220,7 +7254,8 @@ async function handleRequest(req, res) {
|
|
|
7220
7254
|
const bs = run && run.brief_session; const bsess = bs ? sessions.get(bs.id) : null;
|
|
7221
7255
|
const readFailed = !run || brief ? null
|
|
7222
7256
|
: !bs ? { session: null, why: 'the morning-brief session did not start.' }
|
|
7223
|
-
: (!!bs.ended_at || !bsess || bsess.state === 'exited') ? { session: bs.id, why: (bsess && bsess.note) || 'the session ended without submitting a brief.' }
|
|
7257
|
+
: (!!bs.ended_at || !bsess || bsess.state === 'exited') ? { session: bs.id, why: (bsess && bsess.note) || 'the session ended without submitting a brief.' }
|
|
7258
|
+
: briefSessionDone(run, date, now) === 'idle' ? { session: bs.id, idle: true, why: 'the session has printed nothing for two minutes and has not submitted a brief.' } : null;
|
|
7224
7259
|
return json(res, { ok: true, ws: w ? w.id : null, today: date, now: new Date(now).toISOString(), run, stage: dayRunLib.stage(run, brief), brief, plan, cards_error: cardsError, read_failed: readFailed,
|
|
7225
7260
|
settings: { briefSources: st.briefSources, scopes: st.scopes, dayCheck: st.dayCheck, websites: st.websites }, sources,
|
|
7226
7261
|
check: { every: st.dayCheck.every, last: run ? run.checks.last : null, next: due.next, running: !!(run && run.checks.session_id), log: run ? run.checks.log : [] } });
|