@agentwhy/cli 0.2.0 → 0.3.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.
@@ -3,12 +3,16 @@ const HELP_FLAGS = ['--help', '-h'];
3
3
  export class CommandRouter {
4
4
  #commands;
5
5
  #withoutACommand;
6
+ #missingCommand;
6
7
  /**
7
8
  * `withoutACommand` answers `agentwhy` with nothing after it, and with arguments that begin with `-` (they are its
8
9
  * flags); without one, that stays a usage error. It may also be registered under its own name.
10
+ *
11
+ * `missingCommand` is said under that usage error alone, for whoever ran a bare `agentwhy` where nothing answers it.
9
12
  */
10
- constructor(commands, withoutACommand) {
13
+ constructor(commands, withoutACommand, missingCommand) {
11
14
  this.#withoutACommand = withoutACommand;
15
+ this.#missingCommand = missingCommand;
12
16
  const byName = new Map();
13
17
  for (const command of commands) {
14
18
  if (byName.has(command.name))
@@ -27,7 +31,9 @@ export class CommandRouter {
27
31
  if (name !== undefined && HELP_FLAGS.includes(name))
28
32
  return { kind: 'help', usage: this.usage };
29
33
  if (name === undefined) {
30
- return this.#withoutACommand === undefined ? this.#usageError('missing command') : this.#withoutACommand.execute([]);
34
+ if (this.#withoutACommand !== undefined)
35
+ return this.#withoutACommand.execute([]);
36
+ return { ...this.#usageError('missing command'), ...(this.#missingCommand === undefined ? {} : { hint: this.#missingCommand }) };
31
37
  }
32
38
  if (name.startsWith('-') && this.#withoutACommand !== undefined)
33
39
  return this.#withoutACommand.execute(argv);
package/dist/cli.js CHANGED
@@ -71,7 +71,8 @@ function write(result) {
71
71
  process.stdout.write(result.usage);
72
72
  return EXIT_CODE.ok;
73
73
  case 'usage-error':
74
- process.stderr.write(`agentwhy: ${result.message}\n\n${result.usage}`);
74
+ // R73: the message is the first thing on stderr, before any help this one carries.
75
+ process.stderr.write(`agentwhy: ${result.message}\n\n${result.hint === undefined ? '' : `${result.hint}\n\n`}${result.usage}`);
75
76
  return EXIT_CODE.usage;
76
77
  case 'completed':
77
78
  process.stdout.write(result.output);
@@ -71,6 +71,8 @@ import { ConversationsRenderer } from './report/start/conversations/conversation
71
71
  import { SettingsRenderer } from './report/start/settings/settings-renderer.js';
72
72
  import { MonthRenderer } from './report/start/month/month-renderer.js';
73
73
  import { ToFixRenderer } from './report/start/to-fix/to-fix-renderer.js';
74
+ import { projectName } from './report/start/app-nav.js';
75
+ import { welcomeIn } from './report/start/onboarding/welcome-decision.js';
74
76
  import { SessionStart } from './report/start/session-start.js';
75
77
  import { NoticeWordsRenderer } from './report/watch/render/notice-words.js';
76
78
  import { NoticeSettings } from './report/watch/notice-settings.js';
@@ -393,6 +395,19 @@ export function createCommandRouter(environment) {
393
395
  files: start,
394
396
  sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
395
397
  clock: () => Date.now(),
398
+ // AW1: the address `--detach` hands over names the welcome where this project is to meet it, as the address a
399
+ // served run prints does. The same decision the run itself asks (W23), from the same record and settings files.
400
+ welcome: {
401
+ file: APP_LINKS.onboarding,
402
+ project: projectName(environment.workingDirectory),
403
+ opens: async () => (await welcomeIn(environment.workingDirectory, {
404
+ store: onboarding,
405
+ files,
406
+ home: environment.home,
407
+ realHome,
408
+ now: () => Date.now(),
409
+ })).opens,
410
+ },
396
411
  });
397
412
  const start_ = new StartCliCommand({ start, detached, now: environment.now });
398
413
  const check_ = new CheckCliCommand({ check, now: environment.now });
@@ -472,5 +487,10 @@ export function createCommandRouter(environment) {
472
487
  interactive: environment.inputIsTerminal,
473
488
  }),
474
489
  new CodexStopCliCommand(codexWatch, environment.inputIsTerminal),
475
- ], environment.interactive ? start_ : undefined);
490
+ // R73 stands (`a-page-not-a-file` PFD3): off a terminal a bare `agentwhy` opens nothing and serves nothing. But the
491
+ // reader of that refusal is usually an AI agent asked to start agentwhy and send a link, and the usage alone left it
492
+ // guessing: measured 2026-10-04 in three apps, one guessed `start --detach` and one gave up. So the refusal names it.
493
+ ], environment.interactive ? start_ : undefined, 'There is no terminal here, so a bare agentwhy opens nothing and serves nothing.\n' +
494
+ 'To serve a page and return at once - what an agent asked to send someone a link needs - run:\n' +
495
+ ' agentwhy start --detach');
476
496
  }
@@ -21,4 +21,6 @@ export const FOLD_LINE_STYLE = String.raw `
21
21
  .fold-hide{display:none}.fold[open] .fold-hide{display:inline}.fold[open] .fold-show{display:none}
22
22
  .fold-sand>.fold-line{border:1px solid var(--sand-35);background:var(--card)}.fold-sand>.fold-line:hover{border-color:var(--sand)}
23
23
  .fold-sand .fold-mark{width:32px;height:32px;background:var(--sand-16);color:var(--sand)}.fold-sand .fold-mark svg{width:16px;height:16px}
24
+ .fold-grey>.fold-line{border:1px solid var(--white-16);background:var(--card)}.fold-grey>.fold-line:hover{border-color:var(--white-32)}
25
+ .fold-grey .fold-mark{width:32px;height:32px;background:var(--white-10);color:var(--text-2);font-size:15px}
24
26
  `;
@@ -28,7 +28,7 @@ dialog.pp-wide{isolation:isolate}
28
28
  html:not(.js) dialog.pp-wide:target{position:relative}
29
29
  dialog.pp::backdrop{background:var(--backdrop);backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px)}
30
30
  dialog.pp-wizard{max-width:640px;margin-top:56px;max-height:calc(100vh - 112px)}
31
- dialog.pp-list{max-width:800px;margin-top:56px;max-height:calc(100vh - 112px)}
31
+ dialog.pp-list{max-width:920px;margin-top:56px;max-height:calc(100vh - 112px)}
32
32
  dialog.pp-small,dialog.pp-pick,dialog.pp-confirm{border-color:var(--white-10);border-radius:20px;margin:auto}
33
33
  dialog.pp-small{max-width:560px}dialog.pp-pick{max-width:520px}dialog.pp-confirm{max-width:460px}
34
34
  html:not(.js) dialog.pp:target{display:block;position:static;margin:24px auto;max-height:none}
@@ -1,5 +1,5 @@
1
1
  import { escapeHtml as e } from '../html-report-components.js';
2
- import { inLanguages, LANGS, translator } from '../report-copy.js';
2
+ import { inLanguages, labelAttributes, LANGS, translator } from '../report-copy.js';
3
3
  import { initial } from './app-sidebar.js';
4
4
  import { dayName, localClock } from './local-date.js';
5
5
  import { tag } from './tag.js';
@@ -29,19 +29,20 @@ export function projectList(spec) {
29
29
  return pickList(spec, spec.pick, when);
30
30
  const action = spec.action;
31
31
  const row = (one) => '<li class="pjl-row"' + searchable(one) + '>' + cells(one, when, action) + '</li>';
32
- return '<div class="pjl">' +
32
+ return '<div class="pjl" data-order="new">' +
33
33
  (here === undefined ? '' :
34
34
  '<p class="pjl-label">' + inLanguages((t) => t('proj.here')) + '</p>' +
35
35
  '<div class="pjl-here' + (here.setUp === true ? ' pjl-here-set' : '') + '">' + cells(here, when, action) + '</div>') +
36
- '<div class="pjl-label-line"><p class="pjl-label">' + inLanguages((t) => t('proj.others')) + '</p>' +
37
- (others.length + gone.length === 0 ? '' : search()) + '</div>' +
36
+ '<div class="pjl-label-line"><p class="pjl-label">' +
37
+ ['new', 'az', 'za'].map((order) => '<span class="pjl-order pjl-order-' + order + '">' + inLanguages((t) => t(order === 'new' ? 'proj.others' : 'proj.others.' + order)) + '</span>').join('') +
38
+ '</p>' + (others.length + gone.length === 0 ? '' : search()) + '</div>' +
38
39
  '<p class="pjl-none" data-search-none hidden></p>' +
39
40
  (shown.length === 0 && gone.length === 0
40
41
  ? '<p class="pjl-none">' + inLanguages((t) => t('proj.none')) + '</p>'
41
42
  : '<div class="pjl-table">' +
42
43
  (shown.length === 0 ? '' :
43
- '<div class="pjl-heads" aria-hidden="true"><span></span><span>' + inLanguages((t) => t('proj.col.project')) + '</span>' +
44
- '<span>' + inLanguages((t) => t('proj.col.status')) + '</span><span></span></div>' +
44
+ '<div class="pjl-heads"><span aria-hidden="true"></span><span>' + sortByName() + '</span>' +
45
+ '<span aria-hidden="true">' + inLanguages((t) => t('proj.col.status')) + '</span><span aria-hidden="true"></span></div>' +
45
46
  '<ul class="pjl-rows">' + shown.map(row).join('') + '</ul>') +
46
47
  (gone.length === 0 ? '' :
47
48
  '<details class="pjl-gone"><summary class="pjl-gone-line"><span>' + inLanguages((t) => t('proj.goneFold', { n: gone.length })) + '</span>' +
@@ -122,6 +123,18 @@ function search() {
122
123
  return '<input type="search" class="pjl-search js-only" data-project-search autocomplete="off" spellcheck="false"' +
123
124
  ' aria-label="' + e(translator('en')('proj.search')) + '" data-search-words="' + e(JSON.stringify(words)) + '">';
124
125
  }
126
+ /**
127
+ * A-Z, Z-A, or newest first (the maintainer, 2026-10-05): the head of the Project column is the button that cycles the
128
+ * three, with the order it is in drawn beside the word - up for A-Z, down for Z-A, both ways for the order the list
129
+ * came in. Only a script can sort, so only a page with one offers it, and a page without one keeps the plain word; the
130
+ * line above the table says the order in words, so what changed is never only a glyph.
131
+ */
132
+ function sortByName() {
133
+ return '<button type="button" class="pjl-sort js-only" data-pjl-sort' + labelAttributes((t) => t('proj.sort')) + '>' +
134
+ inLanguages((t) => t('proj.col.project')) + '<span class="pjl-sort-mark" aria-hidden="true">' +
135
+ ['new', 'az', 'za'].map((order, at) => '<span class="pjl-order pjl-order-' + order + '">' + ['\u2195', '\u2191', '\u2193'][at] + '</span>').join('') +
136
+ '</span></button><span class="nojs-only" aria-hidden="true">' + inLanguages((t) => t('proj.col.project')) + '</span>';
137
+ }
125
138
  /**
126
139
  * A row's radio, in the column where the window offers **Open**: a real one, so the keys move between the rows as they do
127
140
  * in any group. What the page chooses by is the id, never a path (V17). The row chosen is mint, not coral (the
@@ -131,9 +144,9 @@ function radio(group, row) {
131
144
  return '<input type="radio" class="pjl-radio-input" name="' + e(group) + '" value="' + e(row.id) + '" data-pick-name="' + e(row.name) + '"' +
132
145
  (row.current ? ' data-pick-here checked' : '') + '><span class="pjl-radio" aria-hidden="true"><span class="pjl-radio-dot"></span></span>';
133
146
  }
134
- /** What the field matches a row by: its name and where it is, in lower case. */
147
+ /** What the field matches a row by: its name and where it is, in lower case - and the name alone, what it sorts by. */
135
148
  function searchable(row) {
136
- return ' data-search="' + e((row.name + ' ' + row.place).toLowerCase()) + '"';
149
+ return ' data-search="' + e((row.name + ' ' + row.place).toLowerCase()) + '" data-sort-name="' + e(row.name.toLowerCase()) + '"';
137
150
  }
138
151
  /** A row's four cells: the folder's initial, the project, its status, and what it offers. */
139
152
  function cells(row, when, action) {
@@ -166,8 +179,15 @@ export const PROJECT_LIST_STYLE = String.raw `
166
179
  .pjl{--pjl-columns:44px minmax(0,1fr) 172px 128px}
167
180
  .pjl-label{margin:22px 4px 10px;font-size:13.5px;font-weight:600;color:var(--text-3)}
168
181
  .pjl-label:first-child{margin-top:4px}
169
- .pjl-label-line{display:flex;align-items:flex-end;justify-content:space-between;gap:12px}
170
- .pjl-label-line .pjl-label{margin-top:22px}
182
+ .pjl-label-line{display:flex;align-items:flex-end;justify-content:space-between;gap:12px;margin-top:34px}
183
+ .pjl-label-line .pjl-label{margin-top:0}
184
+ .pjl-order{display:none}
185
+ .pjl[data-order="new"] .pjl-order-new,.pjl[data-order="az"] .pjl-order-az,.pjl[data-order="za"] .pjl-order-za{display:inline}
186
+ .pjl-sort{display:inline-flex;align-items:center;gap:7px;margin:-4px 0;padding:4px 8px;border:0;border-radius:8px;background:none;font:inherit;font-size:12.5px;font-weight:600;color:var(--text-3);cursor:pointer}
187
+ .pjl-sort:hover{color:var(--text);background:var(--white-05)}
188
+ .pjl-sort:focus-visible{outline:2px solid var(--coral);outline-offset:1px}
189
+ .pjl-sort-mark{font-size:12px;line-height:1;opacity:.75}
190
+ .pjl-sort:hover .pjl-sort-mark{opacity:1}
171
191
  .pjl-search{width:200px;margin:0 0 8px;padding:7px 12px;border-radius:999px;border:1px solid var(--white-14);background:var(--card);color:var(--text);font:inherit;font-size:13.5px}
172
192
  .pjl-search::placeholder{color:var(--text-3)}
173
193
  .pjl-search:focus{outline:none;border-color:var(--white-32)}
@@ -248,6 +268,25 @@ export const PROJECT_LIST_SCRIPT = String.raw `
248
268
  const list = button.closest('.pjl');
249
269
  if (list) list.classList.remove('pjl-folded');
250
270
  }));
271
+ // A-Z, Z-A, newest first: the order the rows came in is kept, so the third press is not a guess at it.
272
+ document.querySelectorAll('[data-pjl-sort]').forEach((button) => {
273
+ const list = button.closest('.pjl');
274
+ if (!list) return;
275
+ const after = { new: 'az', az: 'za', za: 'new' };
276
+ const groups = [...list.querySelectorAll('.pjl-table .pjl-rows')].map((rows) => ({ rows, came: [...rows.children] }));
277
+ button.addEventListener('click', () => {
278
+ const order = after[list.dataset.order] || 'az';
279
+ list.dataset.order = order;
280
+ groups.forEach(({ rows, came }) => {
281
+ const put = came.slice();
282
+ if (order !== 'new') {
283
+ const way = order === 'az' ? 1 : -1;
284
+ put.sort((one, two) => way * String(one.dataset.sortName || '').localeCompare(String(two.dataset.sortName || '')));
285
+ }
286
+ put.forEach((row) => rows.appendChild(row));
287
+ });
288
+ });
289
+ });
251
290
  document.querySelectorAll('[data-project-search]').forEach((input) => {
252
291
  const list = input.closest('.pjl');
253
292
  if (!list) return;
@@ -68,6 +68,10 @@ export const CONVERSATIONS_WORDS = {
68
68
  'conv.unchecked.partial.title.other': '{n} conversations couldn’t be fully checked.',
69
69
  'conv.unchecked.partial': 'Their record leaves out some steps, so their reports may not show everything your AI opened. Open one to see what was written down.',
70
70
  'conv.unchecked.mixed': 'Some are older than this check, or their record couldn’t be opened. The others leave out some steps, so their reports may not show everything. To include older ones, run {command}',
71
+ 'conv.unchecked.lead.rest': '— older than this check, or their record couldn’t be read.',
72
+ 'conv.unchecked.check.rest': '— not read yet.',
73
+ 'conv.unchecked.partial.rest': '— read, but their record has gaps.',
74
+ 'conv.unchecked.mixed.rest': '— some older than this check, the rest read with gaps.',
71
75
  'conv.act.fix': 'Fix it →',
72
76
  'conv.act.see': 'See report',
73
77
  'conv.act.none': 'No report',
@@ -226,6 +230,10 @@ export const CONVERSATIONS_WORDS = {
226
230
  'conv.unchecked.partial.title.many': '{n} rozmów nie zostało w pełni sprawdzonych.',
227
231
  'conv.unchecked.partial': 'Ich zapis pomija niektóre kroki, więc raporty mogą nie pokazywać wszystkiego, co otworzyło twoje AI. Otwórz którąś, żeby zobaczyć, co zostało zapisane.',
228
232
  'conv.unchecked.mixed': 'Część jest starsza niż to sprawdzenie albo nie dało się otworzyć ich zapisu. Zapis pozostałych pomija niektóre kroki, więc raporty mogą nie pokazywać wszystkiego. Żeby dołączyć starsze, uruchom {command}',
233
+ 'conv.unchecked.lead.rest': '— starsze niż to sprawdzenie albo nie dało się odczytać ich zapisu.',
234
+ 'conv.unchecked.check.rest': '— jeszcze nieprzeczytane.',
235
+ 'conv.unchecked.partial.rest': '— przeczytane, ale ich zapis ma luki.',
236
+ 'conv.unchecked.mixed.rest': '— część starsza niż to sprawdzenie, reszta przeczytana z lukami.',
229
237
  'conv.act.fix': 'Napraw →',
230
238
  'conv.act.see': 'Zobacz raport',
231
239
  'conv.act.none': 'Brak raportu',
@@ -376,6 +384,10 @@ export const CONVERSATIONS_WORDS = {
376
384
  'conv.unchecked.partial.title.other': '{n} Unterhaltungen konnten nicht vollständig geprüft werden.',
377
385
  'conv.unchecked.partial': 'Ihre Aufzeichnung lässt einige Schritte aus, daher zeigen ihre Berichte vielleicht nicht alles, was deine KI geöffnet hat. Öffne eine, um zu sehen, was aufgezeichnet wurde.',
378
386
  'conv.unchecked.mixed': 'Einige sind älter als diese Prüfung, oder ihre Aufzeichnung ließ sich nicht öffnen. Die übrigen lassen einige Schritte aus, daher zeigen ihre Berichte vielleicht nicht alles. Um ältere einzubeziehen, führe {command} aus',
387
+ 'conv.unchecked.lead.rest': '— älter als diese Prüfung, oder ihre Aufzeichnung war nicht lesbar.',
388
+ 'conv.unchecked.check.rest': '— noch nicht gelesen.',
389
+ 'conv.unchecked.partial.rest': '— gelesen, aber ihre Aufzeichnung hat Lücken.',
390
+ 'conv.unchecked.mixed.rest': '— einige älter als diese Prüfung, die übrigen mit Lücken gelesen.',
379
391
  'conv.act.fix': 'Beheben →',
380
392
  'conv.act.see': 'Bericht ansehen',
381
393
  'conv.act.none': 'Kein Bericht',
@@ -9,6 +9,9 @@ export const PROJECTS_WORDS = {
9
9
  'proj.lead': 'Pick the project you want agentwhy to watch.',
10
10
  'proj.here': 'You’re here now',
11
11
  'proj.others': 'Other projects · newest first',
12
+ 'proj.others.az': 'Other projects · A–Z',
13
+ 'proj.others.za': 'Other projects · Z–A',
14
+ 'proj.sort': 'Sort projects by name',
12
15
  'proj.yours': 'Your projects · newest first',
13
16
  'moved.pageTitle': 'Another project · agentwhy',
14
17
  'moved.title': 'This page was for {was}.',
@@ -76,6 +79,9 @@ export const PROJECTS_WORDS = {
76
79
  'proj.lead': 'Wybierz projekt, który agentwhy ma obserwować.',
77
80
  'proj.here': 'Jesteś tutaj',
78
81
  'proj.others': 'Inne projekty · od najnowszych',
82
+ 'proj.others.az': 'Inne projekty · A–Z',
83
+ 'proj.others.za': 'Inne projekty · Z–A',
84
+ 'proj.sort': 'Sortuj projekty po nazwie',
79
85
  'proj.yours': 'Twoje projekty · od najnowszych',
80
86
  'moved.pageTitle': 'Inny projekt · agentwhy',
81
87
  'moved.title': 'Ta strona była dla projektu {was}.',
@@ -152,6 +158,9 @@ export const PROJECTS_WORDS = {
152
158
  'proj.lead': 'Wähle das Projekt, das agentwhy beobachten soll.',
153
159
  'proj.here': 'Hier bist du gerade',
154
160
  'proj.others': 'Andere Projekte · die neuesten zuerst',
161
+ 'proj.others.az': 'Andere Projekte · A–Z',
162
+ 'proj.others.za': 'Andere Projekte · Z–A',
163
+ 'proj.sort': 'Projekte nach Name sortieren',
155
164
  'proj.yours': 'Deine Projekte · die neuesten zuerst',
156
165
  'moved.pageTitle': 'Anderes Projekt · agentwhy',
157
166
  'moved.title': 'Diese Seite war für {was}.',
@@ -78,12 +78,7 @@ export function periodSection(periods, at, index, spec) {
78
78
  dataTable({ ...CONVERSATION_TABLE, rows: fix.map((item) => conversationRow(item, index.widen, !index.shared)) }) +
79
79
  '</section>') +
80
80
  (!spec.lists || info.length === 0 ? '' : forYourInfo(info, index)) +
81
- (!spec.lists || unchecked.length === 0 ? '' :
82
- '<section class="cw-need cw-unchecked" data-need><div class="cw-need-head"><span class="cw-need-dot" aria-hidden="true"></span>' +
83
- '<h2 class="cw-h2">' + inLanguages((t) => t(partial ? 'look.unchecked' : 'conv.unchecked')) + '</h2><span class="cw-need-count" data-need-count>' + unchecked.length + '</span></div>' +
84
- '<p class="cw-unchecked-lead">' + inLanguages((t) => t(lead, { command: '<code>' + e(index.widen) + '</code>' })) + '</p>' +
85
- dataTable({ ...CONVERSATION_TABLE, rows: unchecked.map((item) => conversationRow(item, index.widen, !index.shared)) }) +
86
- '</section>') +
81
+ (!spec.lists || unchecked.length === 0 ? '' : notFullyChecked(unchecked, partial, lead, index)) +
87
82
  (!spec.lists || others.length === 0 ? '' : theRest(others, need.length === 0, index)) +
88
83
  '</section>';
89
84
  }
@@ -101,6 +96,27 @@ function forYourInfo(info, index) {
101
96
  body: dataTable({ ...CONVERSATION_TABLE, rows: info.map((item) => conversationRow(item, index.widen, !index.shared)) }),
102
97
  }) + '</section>';
103
98
  }
99
+ /**
100
+ * "Couldn't check fully (n)" (the maintainer, 2026-10-05): what this run could not read, or read with gaps, in a fold
101
+ * of its own - a solid grey line with the look's ?, shut, and opened with **Show**. Every Codex record has a gap
102
+ * (`2026-09-27-what-codex-wrote.md` X23), so on a project worked in with Codex this list holds the whole week, asks
103
+ * for nothing, and was the longest thing on the page; the maintainer asked for it shut on arrival (2026-10-05), not
104
+ * only shuttable. Shut it still says all of it the page owes: the heading, how many, and in brief why - and the
105
+ * period's card above says the number too. The sentence of the reason and the command that would include them go
106
+ * inside, with the rows; the list is still apart from what is to fix and is never a group of the rest's table
107
+ * (F17, F13, O4). The script narrows it to the day chosen as it does the other lists (`data-need`), and the count it
108
+ * rewrites is the one in the line, which is in view whether the fold is open or not.
109
+ */
110
+ function notFullyChecked(unchecked, partial, lead, index) {
111
+ return '<section class="cw-need cw-unchecked" data-need>' + foldLine({
112
+ summary: '<strong>' + inLanguages((t) => t(partial ? 'look.unchecked' : 'conv.unchecked')) +
113
+ ' (<span data-need-count>' + unchecked.length + '</span>)</strong> <span class="fold-rest">' +
114
+ inLanguages((t) => t(lead + '.rest')) + '</span>',
115
+ mark: { glyph: LOOKS.unchecked.glyph, tone: 'grey' },
116
+ body: '<p class="cw-unchecked-lead">' + inLanguages((t) => t(lead, { command: '<code>' + e(index.widen) + '</code>' })) + '</p>' +
117
+ dataTable({ ...CONVERSATION_TABLE, rows: unchecked.map((item) => conversationRow(item, index.widen, !index.shared)) }),
118
+ }) + '</section>';
119
+ }
104
120
  /** The looks of the rest, in the order of F16's ladder: the groups of F13 and F14, and the order they are listed in. */
105
121
  const REST = ['fixed', 'name', 'stopped', 'none'];
106
122
  /** A group's table rows past this many wait behind "Show all" while the whole period is shown (F14). */
@@ -237,8 +253,7 @@ export const PERIOD_SECTION_STYLE = String.raw `
237
253
  .cw-need-head{display:flex;align-items:baseline;gap:10px;margin-bottom:12px}
238
254
  .cw-need-dot{width:10px;height:10px;border-radius:50%;background:var(--coral);align-self:center}
239
255
  .cw-need-count{font-size:15px;font-weight:650;color:var(--coral-text)}
240
- .cw-unchecked .cw-need-dot{background:var(--text-3)}.cw-unchecked .cw-need-count{color:var(--text-2)}
241
- .cw-unchecked-lead{margin:-4px 0 12px;font-size:14px;line-height:1.5;color:var(--text-2)}
256
+ .cw-unchecked-lead{margin:0 0 12px;font-size:14px;line-height:1.5;color:var(--text-2)}
242
257
  .cw-unchecked-lead code,.guide code{font-family:var(--mono);font-size:13px;color:var(--text);background:var(--white-07);border-radius:6px;padding:1px 6px}
243
258
  .cw-tools{display:none;gap:10px;flex-wrap:wrap;align-items:center;margin-bottom:16px}
244
259
  .js .cw-tools{display:flex}
@@ -1,3 +1,4 @@
1
+ import { printable } from '../../shared/printable.js';
1
2
  import { quietlySaid } from './render/start-words.js';
2
3
  /**
3
4
  * How long a server started in the background is waited for, at most, while its process runs (PF3): its first pages are
@@ -34,12 +35,16 @@ export class DetachedStart {
34
35
  }
35
36
  return this.#asFiles(options);
36
37
  }
37
- /** The page asked for on a running server: the session's report where it serves one, else every conversation (SW10). */
38
+ /**
39
+ * The page asked for on a running server: the session's report where it serves one (SW10), else the welcome where
40
+ * this project is to meet it (AW1), else every conversation.
41
+ */
38
42
  async #show(base, options, reused) {
39
43
  const { probe, browser } = this.#dependencies;
40
44
  const asked = options.session === undefined ? undefined : `${base}${options.session}.html`;
41
45
  const found = asked === undefined || (await probe.answers(asked));
42
- const url = asked !== undefined && found ? asked : `${base}index.html`;
46
+ const welcome = await this.#welcome(asked);
47
+ const url = asked !== undefined && found ? asked : `${base}${welcome?.file ?? 'index.html'}`;
43
48
  const opened = options.open && (await browser.open(url));
44
49
  // A conversation asked for and not served is said in every output, never passed off as its report (SW10).
45
50
  const missing = found ? '' : 'That conversation was not found here, so all conversations are served.\n';
@@ -50,9 +55,23 @@ export class DetachedStart {
50
55
  outcome: 'written',
51
56
  output: missing +
52
57
  `${reused ? 'agentwhy was already running for this project' : 'agentwhy runs in the background for this project'}: ${url}\n` +
58
+ // AWD3's sentence, in the words a served run already prints: true whether or not anyone opens the address.
59
+ (welcome === undefined
60
+ ? ''
61
+ : `${opened ? 'Opened' : 'That address opens'} the welcome page, to set agentwhy up for ${printable(welcome.project)}.\n`) +
53
62
  'It stops by itself 30 minutes after its last page is closed.\n',
54
63
  };
55
64
  }
65
+ /**
66
+ * AW1: the onboarding's file where this run's address is to name it. A conversation asked for is the person's own
67
+ * yes to that report and wins over a setup they did not ask for, as a served run's own page does (SW10).
68
+ */
69
+ async #welcome(asked) {
70
+ const { welcome } = this.#dependencies;
71
+ if (asked !== undefined || welcome === undefined || !(await welcome.opens()))
72
+ return undefined;
73
+ return { file: welcome.file, project: welcome.project };
74
+ }
56
75
  /** PF4: no server came up - a sandbox, a refusal - so the pages are files, and what would serve them is said. */
57
76
  async #asFiles(options) {
58
77
  const written = await this.#dependencies.files.run({ ...options, serve: false });
@@ -0,0 +1,44 @@
1
+ // Copyright 2026 Nessprim Karol Kozer
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ import { join } from 'node:path';
4
+ import { SETTINGS_FILES } from '../../../adapter/claude-code/contract/settings.js';
5
+ import { notAProject } from '../../../setup/not-a-project.js';
6
+ import { readSettingsFile, setUpBy } from '../settings-files.js';
7
+ /**
8
+ * Whether a project's run should name the onboarding instead of the index (W23), and what else the page needs to know.
9
+ *
10
+ * Its one side effect is N6's: a project set up before this screen existed, or by hand, has its `done` line written
11
+ * here and silently, so that turning alerts off later never brings the onboarding back. Asking a second time for the
12
+ * same project writes no second line: that reading finds the first.
13
+ *
14
+ * Shared by every run that names a page - `start` itself, and `start --detach`, whose address is the only way a person
15
+ * reaches one (`2026-10-01-the-address-opens-the-welcome.md` AW1, extended to `--detach` on 2026-10-04).
16
+ */
17
+ export async function welcomeIn(workingDirectory, asked) {
18
+ const { store, files, home, realHome, now } = asked;
19
+ const reading = await store.read();
20
+ // W24: the intro plays once per person - where no project of theirs has finished the onboarding.
21
+ const intro = !reading.anywhere && !reading.failed;
22
+ // which-project V7: in the home directory no project is being shown, so the onboarding opens at its project step,
23
+ // whatever the record says - after the welcome only for a person who has never finished one. Nothing here is set up
24
+ // and no record is kept of it: not even the silent `done` line below.
25
+ if (home !== undefined && notAProject(workingDirectory, home, realHome) !== undefined) {
26
+ return { opens: true, intro, setUp: false, ...(intro ? {} : { atProject: true }) };
27
+ }
28
+ if (reading.here)
29
+ return { opens: false, intro, setUp: true };
30
+ if (reading.failed)
31
+ return { opens: false, intro, setUp: false };
32
+ // A local file that cannot be read leaves what runs unknown, and the onboarding closed (as `#settingsNow` does).
33
+ const local = await readSettingsFile(files, join(workingDirectory, SETTINGS_FILES.directory, SETTINGS_FILES.local));
34
+ if (local === 'unreadable')
35
+ return { opens: false, intro, setUp: false };
36
+ const shared = await readSettingsFile(files, join(workingDirectory, SETTINGS_FILES.directory, SETTINGS_FILES.shared));
37
+ // N6, widened 2026-09-28 by the maintainer (which-project V10): a project where one of agentwhy's hooks runs, or whose
38
+ // settings block files, was set up - before this screen existed, or by hand - and is recorded so, silently.
39
+ if (setUpBy([local, shared])) {
40
+ await store.add(now());
41
+ return { opens: false, intro, setUp: true };
42
+ }
43
+ return { opens: true, intro, setUp: false };
44
+ }
@@ -75,7 +75,12 @@ function page(said, view) {
75
75
  ...(said.welcome === 'opened' ? [`Opened the welcome page, to set agentwhy up for ${said.project ?? 'this project'}.`] : []),
76
76
  ...(said.welcome === 'served' ? [`That address opens the welcome page, to set agentwhy up for ${said.project ?? 'this project'}.`] : []),
77
77
  ...(said.servedAsFile === true
78
- ? [warn('The page could not be served, so it was opened as a file: marks made on it are copied as commands.', view)]
78
+ ? [
79
+ warn('The page could not be served, so it was opened as a file: marks made on it are copied as commands.', view),
80
+ // PF4's way out, which `--detach` has always said and this run did not: a sandbox is the usual reason, and a
81
+ // person reading a file cannot finish a setup or make a mark on it.
82
+ dim('To open it live, run this command again outside the sandbox, or type npx @agentwhy/cli in a terminal.', view),
83
+ ]
79
84
  : []),
80
85
  dim(said.shared
81
86
  ? 'Shared view: session ids and the project path are not shown, and nothing above the project root appears in any report.'
@@ -19,7 +19,7 @@ import { indexHandler } from './serve/index-handler.js';
19
19
  import { movedPage } from './projects/moved-page.js';
20
20
  import { nearestProject } from '../../core/nearest-project.js';
21
21
  import { translator } from '../render/report-copy.js';
22
- import { hookComplete, readSettingsFile, setUpBy } from './settings-files.js';
22
+ import { hookComplete, readSettingsFile } from './settings-files.js';
23
23
  import { indexProjects } from './projects/index-projects.js';
24
24
  import { NOT_A_PROJECT } from '../../setup/project-setup.js';
25
25
  import { notAProject } from '../../setup/not-a-project.js';
@@ -31,6 +31,7 @@ import { DEFAULT_THRESHOLD } from '../watch/agent-alert.js';
31
31
  import { DEFAULT_CHANNELS, DEFAULT_CLEAN, DEFAULT_SAID_AS } from '../watch/notice-choices.js';
32
32
  import { actionsAfterMarks, markLines, marksInRange, standingMarks } from '../check/marks.js';
33
33
  import { finishOnboarding } from './onboarding/finish-onboarding.js';
34
+ import { welcomeIn } from './onboarding/welcome-decision.js';
34
35
  import { WHO_STEP } from './onboarding/onboarding-script.js';
35
36
  import { repositoryAbove } from './repository.js';
36
37
  import { pageStopped, quietlySaid, startSaid } from './render/start-words.js';
@@ -684,33 +685,17 @@ export class SessionStart {
684
685
  * alerts off later never brings the onboarding back.
685
686
  */
686
687
  async #welcome(workingDirectory) {
687
- const { onboarding, setup, notices } = this.#dependencies;
688
+ const { onboarding, setup, notices, policyFiles, home, realHome } = this.#dependencies;
689
+ // The onboarding writes through both (W15), so a run without them names no page it could not finish.
688
690
  if (onboarding === undefined || setup === undefined || notices === undefined)
689
691
  return undefined;
690
- const reading = await onboarding.store.read();
691
- // W24: the intro plays once per person - where no project of theirs has finished the onboarding.
692
- const intro = !reading.anywhere && !reading.failed;
693
- // which-project V7: in the home directory no project is being shown, so the onboarding opens at its project step,
694
- // whatever the record says - after the welcome only for a person who has never finished one. Nothing here is set up
695
- // and no record is kept of it: not even the silent `done` line below.
696
- if (this.#notAProject(workingDirectory) !== undefined)
697
- return { opens: true, intro, setUp: false, ...(intro ? {} : { atProject: true }) };
698
- if (reading.here)
699
- return { opens: false, intro, setUp: true };
700
- if (reading.failed)
701
- return { opens: false, intro, setUp: false };
702
- // A local file that cannot be read leaves what runs unknown, and the onboarding closed (as `#settingsNow` does).
703
- const local = await this.#settingsFile(join(workingDirectory, SETTINGS_FILES.directory, SETTINGS_FILES.local));
704
- if (local === 'unreadable')
705
- return { opens: false, intro, setUp: false };
706
- const shared = await this.#settingsFile(join(workingDirectory, SETTINGS_FILES.directory, SETTINGS_FILES.shared));
707
- // N6, widened 2026-09-28 by the maintainer (which-project V10): a project where one of agentwhy's hooks runs, or whose
708
- // settings block files, was set up - before this screen existed, or by hand - and is recorded so, silently.
709
- if (setUpBy([local, shared])) {
710
- await onboarding.store.add((this.#dependencies.clock ?? (() => this.#dependencies.now))());
711
- return { opens: false, intro, setUp: true };
712
- }
713
- return { opens: true, intro, setUp: false };
692
+ return welcomeIn(workingDirectory, {
693
+ store: onboarding.store,
694
+ files: policyFiles,
695
+ ...(home === undefined ? {} : { home }),
696
+ ...(realHome === undefined ? {} : { realHome }),
697
+ now: this.#dependencies.clock ?? (() => this.#dependencies.now),
698
+ });
714
699
  }
715
700
  /**
716
701
  * The person's projects, read again with every refresh as the settings are, since a project set up in another tab is
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentwhy/cli",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Reconstructs from transcripts why an AI agent reached for protected data",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",