@agentwhy/cli 0.2.1 → 0.3.1

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.
Files changed (38) hide show
  1. package/dist/adapter/codex/contract/assumptions.js +4 -4
  2. package/dist/adapter/codex/contract/deliveries.js +12 -0
  3. package/dist/adapter/codex/contract/messages.js +8 -1
  4. package/dist/adapter/codex/events/cell-commands.js +191 -0
  5. package/dist/adapter/codex/events/delivered-output.js +30 -0
  6. package/dist/adapter/codex/events/rollout-scan.js +99 -7
  7. package/dist/core/access/command-line.js +4 -1
  8. package/dist/core/access/listing.js +10 -3
  9. package/dist/core/access/protected-access.js +94 -1
  10. package/dist/core/access/recorded-effect.js +22 -1
  11. package/dist/report/build-report.js +125 -19
  12. package/dist/report/render/report-page/file-story.js +33 -2
  13. package/dist/report/render/report-page/files-view.js +176 -55
  14. package/dist/report/render/report-page/files.js +51 -8
  15. package/dist/report/render/report-page/fix-wizard-script.js +17 -7
  16. package/dist/report/render/report-page/fix-wizard.js +4 -1
  17. package/dist/report/render/report-page/helpers.js +2 -1
  18. package/dist/report/render/report-page/report-page-renderer.js +51 -12
  19. package/dist/report/render/report-page/story-window.js +33 -12
  20. package/dist/report/render/report-page/to-do-view.js +42 -10
  21. package/dist/report/render/ui/app-sidebar.js +31 -2
  22. package/dist/report/render/ui/confirm-dialog.js +69 -4
  23. package/dist/report/render/ui/fold-line.js +2 -0
  24. package/dist/report/render/ui/popup.js +1 -1
  25. package/dist/report/render/ui/project-list.js +49 -10
  26. package/dist/report/render/ui/words/app-words.js +6 -0
  27. package/dist/report/render/ui/words/conversations-words.js +12 -0
  28. package/dist/report/render/ui/words/projects-words.js +9 -0
  29. package/dist/report/render/ui/words/report-words.js +114 -12
  30. package/dist/report/render/ui/words/settings-words.js +12 -9
  31. package/dist/report/start/conversations/calendar-view.js +1 -1
  32. package/dist/report/start/conversations/conversation-columns.js +6 -4
  33. package/dist/report/start/conversations/period-section.js +33 -17
  34. package/dist/report/start/conversations/periods.js +7 -1
  35. package/dist/report/start/render/session-status.js +14 -9
  36. package/dist/report/start/session-start.js +6 -3
  37. package/dist/report/start/settings/alerts-tab.js +2 -1
  38. package/package.json +1 -1
@@ -24,8 +24,12 @@ import { fixId, storyId } from './to-do-view.js';
24
24
  * `protectKeys` names the files **Protect it** is offered for - private, not protected yet, and inside the project - and
25
25
  * the everyday ones inside it, offered **Make it private**.
26
26
  */
27
- export function filesView(rows, items, protectKeys, names) {
27
+ export function filesView(rows, items, protectKeys, names, leftOut = 0) {
28
28
  const entries = rows.map((row, key) => ({ row, key }));
29
+ // Names past the most a page lists are said as a number, never dropped in silence (invariant 4).
30
+ const more = leftOut === 0 ? '' : '<p class="fl-note">' + inLanguages((t) => t('fl.namesLeftOut', { n: leftOut })) + '</p>';
31
+ // OW4 as amended 2026-10-05: an order is offered only where the files came up at more than one moment.
32
+ const orderable = momentsOf(rows) > 1;
29
33
  const opened = entries.filter(({ row }) => !onlyNamed(row));
30
34
  // One list, in the order a person deals with it (the maintainer, 2026-09-24): what the AI read, in coral; then the
31
35
  // private files it only saw the name of, in blue; then everything else. A name only seen is still never counted as
@@ -33,7 +37,8 @@ export function filesView(rows, items, protectKeys, names) {
33
37
  const listed = [...entries].sort((a, b) => tierOf(a.row) - tierOf(b.row));
34
38
  if (opened.length === 0) {
35
39
  return '<div class="fl"><div class="rp-alone">' + guideCard({ tone: 'mint', title: inLanguages((t) => t('fl.noneListed')), body: inLanguages((t) => t('fl.commands')) }) +
36
- '</div>' + (listed.length === 0 ? '' : table(listed, items, protectKeys, false, names)) + '</div>';
40
+ '</div>' + (listed.length === 0 ? '' : (orderable ? '<div class="fl-filters js-only"><div class="fl-row">' + inLanguages(orderChoice) + '</div></div>' : '') +
41
+ table(listed, items, protectKeys, false, names, orderable)) + more + '</div>';
37
42
  }
38
43
  const privates = opened.filter(({ row }) => row.private).length;
39
44
  const action = privates === 0 ? inLanguages((t) => t('fl.noneRead'))
@@ -42,14 +47,27 @@ export function filesView(rows, items, protectKeys, names) {
42
47
  const lead = privates === 0 ? 'fl.leadNone' : privates === opened.length ? 'fl.leadAll' : 'fl.lead';
43
48
  return '<div class="fl' + (privates === 0 ? ' fl-clean' : '') + '">' +
44
49
  hero({ eyebrow: inLanguages((t) => t('fl.eyebrow')), fact: inLanguages((t) => t('fl.fact', { n: opened.length })), action, lead: inLanguages((t) => t(lead)) }) +
45
- filters(listed.map(({ row }) => row)) + table(listed, items, protectKeys, true, names) +
50
+ filters(listed.map(({ row }) => row), orderable) + table(listed, items, protectKeys, true, names, orderable) +
46
51
  '<p class="fl-note">' + inLanguages((t) => t('fl.shown', { shown: '<span data-files-shown>' + listed.length + '</span>', n: listed.length })) + ' ' +
47
- inLanguages((t) => t('fl.commands')) + '</p></div>';
52
+ inLanguages((t) => t('fl.commands')) + '</p>' + more + '</div>';
48
53
  }
49
- /** The table's three parts, in the order a person deals with them: read (coral), only its name seen (blue), the rest. */
50
- const TIERS = [{ key: 'fl.tier.read', tone: 'coral' }, { key: 'fl.tier.name', tone: 'blue' }, { key: 'fl.tier.rest', tone: 'grey' }];
54
+ /**
55
+ * The table's parts, in the order a person deals with them (guidelines §9: the incident first, then what was stopped,
56
+ * then what is fine): read (coral), not known whether read (amber), stopped (mint), only its name seen (blue), the rest.
57
+ */
58
+ const TIERS = [
59
+ { key: 'fl.tier.read', tone: 'coral' }, { key: 'fl.tier.unknown', tone: 'amber' }, { key: 'fl.tier.stopped', tone: 'mint' },
60
+ { key: 'fl.tier.name', tone: 'blue' }, { key: 'fl.tier.rest', tone: 'grey' },
61
+ ];
51
62
  function tierOf(row) {
52
- return row.private && (row.access === 'read' || row.access === 'changed') ? 0 : onlyNamed(row) ? 1 : 2;
63
+ // Only a private file has a part of its own; an everyday file, whatever was done to it, is part of the rest (P32,
64
+ // 2026-10-05). A private file stopped or of no known end was listed with the rest until 2026-10-05, so a conversation
65
+ // the AI was stopped in drew one part, and no heading at all.
66
+ if (!row.private)
67
+ return 4;
68
+ if (row.access === 'read' || row.access === 'changed')
69
+ return 0;
70
+ return row.access === 'unknown' ? 1 : row.access === 'stopped' ? 2 : onlyNamed(row) ? 3 : 4;
53
71
  }
54
72
  /**
55
73
  * The window of every row not on the to-do list, written beside the page's other windows, outside the views. A private
@@ -61,8 +79,11 @@ export function fileWindows(rows, protectKeys, report, clock, names) {
61
79
  return rows.map((row, key) => {
62
80
  if (row.item !== undefined)
63
81
  return '';
64
- const story = row.private ? fileStory(report, row.path) : undefined;
65
- return story === undefined || story.entries.length === 0 ? fileWindow(row, key, protectKeys, names) : rowStoryWindow(row, key, story, report, protectKeys, clock, names);
82
+ // `the-same-window-for-every-file` EF7: the window follows what the model holds for the row, not whether the file is
83
+ // private. An everyday file the AI read, changed or was stopped from has its calls (EF2) and tells the same story; a
84
+ // name only seen has none kept (EFD1) and keeps the simple window, as a file the flows name nowhere always has.
85
+ const story = fileStory(report, row.path);
86
+ return story.entries.length === 0 ? fileWindow(row, key, protectKeys, names) : rowStoryWindow(row, key, story, report, protectKeys, clock, names);
66
87
  }).join('');
67
88
  }
68
89
  /** A private file with nothing to fix, in the story window: its row's words for what happened and what to do. */
@@ -71,7 +92,9 @@ function rowStoryWindow(row, key, story, report, protectKeys, clock, names) {
71
92
  const read = report.privateFiles.find((file) => file.path === row.path);
72
93
  const pattern = report.findings.find((finding) => finding.path === row.path)?.pattern;
73
94
  const reach = LOOKS[LOOK[row.access === 'changed' ? 'read' : row.access]];
74
- const exposed = row.access === 'read' || row.access === 'changed';
95
+ // A copy reached the AI, so the diagram ends at the company. A write is not that: for a private file it has always
96
+ // followed a read, and for an everyday file (EF1) nothing says what was inside before it was written.
97
+ const exposed = row.access === 'read' || (row.private && row.access === 'changed');
75
98
  const protectKey = protectKeys.get(row.path);
76
99
  return storyPopup(story, {
77
100
  path: row.path,
@@ -79,11 +102,18 @@ function rowStoryWindow(row, key, story, report, protectKeys, clock, names) {
79
102
  title: label(row.kind),
80
103
  ...(pattern === undefined ? {} : { pattern }),
81
104
  exposed,
82
- summary: label('fl.w.did.' + row.access),
105
+ ...(row.private ? {} : { everyday: true }),
106
+ summary: label(didKey(row)),
83
107
  ...(exposed ? {} : { look: { glyph: reach.glyph, tone: reach.tone } }),
84
108
  }, 'file-' + key, report.scope.sessionId, clock, { context: label(nextOf(row)), names }, protectKey === undefined ? '' : '<span data-protect-open="' + protectKey + '">' +
85
109
  pill({ label: label(protectLabel(row)), tone: 'light', size: 'md', href: '#protect-' + protectKey, attributes: opener('protect-' + protectKey) }) + '</span>');
86
110
  }
111
+ /** What the AI did to a file, in its window's words: an everyday file's read and stop have words of their own. */
112
+ function didKey(row) {
113
+ if (row.private)
114
+ return 'fl.w.did.' + row.access;
115
+ return row.access === 'changed' ? 'fl.w.did.changed' : row.access === 'stopped' ? 'fl.w.did.everydayStopped' : 'fl.w.did.everydayRead';
116
+ }
87
117
  /** What to do about a file with nothing on the to-do list: the last answer of its window. */
88
118
  function nextOf(row) {
89
119
  return !row.private ? 'fl.w.next.none' : row.protection === 'yes' ? 'fl.w.next.safe' : row.protection === 'told' ? 'fl.w.next.told' : 'fl.w.next.maybe';
@@ -102,8 +132,54 @@ const PROTECTED = {
102
132
  function didOf(row) {
103
133
  return row.access === 'read' || row.access === 'name' || row.access === 'stopped' ? row.access : 'other';
104
134
  }
135
+ /**
136
+ * OW4: the order of the rows - what needs the person first, as drawn, or the order the AI went - asked as a question,
137
+ * as the filters are, and a script's: without one the rows stay as drawn (OW6).
138
+ */
139
+ function orderChoice(t) {
140
+ return labelledSelect(e(t('fl.order.q')), [
141
+ { value: 'need', label: e(t('fl.order.need')) },
142
+ { value: 'steps', label: e(t('fl.order.steps')) },
143
+ ], ' data-files-sort data-live-keep');
144
+ }
145
+ /** The moments the rows came up at: each AI's times it came across files, and one more for rows with none known. */
146
+ function momentsOf(rows) {
147
+ return new Set(rows.map((row) => (row.step === undefined ? 'none' : row.step.rank + ':' + row.step.number))).size;
148
+ }
149
+ /**
150
+ * OW3 as amended 2026-10-05: a heading over the files of each moment, in words - "First", then "Then" - shown only in
151
+ * the order the AI went, where its rows are put under it. A helper's are named for it. Drawn hidden after the rows: the
152
+ * script moves each to its moment, before the moment's first file, and shows it.
153
+ */
154
+ /**
155
+ * EF6: one moment in words - "First", "Then", a helper's - the same words the table heads its moments with (OW3), for
156
+ * the window of a row that is not ordered beside anything. An action number is Advanced's alone (guidelines §7).
157
+ */
158
+ function momentWords(step) {
159
+ return inLanguages((t) => step === undefined ? t('fl.order.unknown')
160
+ : step.helper === undefined ? t(step.number === 1 ? 'fl.order.first' : 'fl.order.then')
161
+ : t(step.number === 1 ? 'fl.order.helperFirst' : 'fl.order.helperThen', { helper: t('st.helper', { ordinal: step.helper }) }));
162
+ }
163
+ function momentHeadings(rows) {
164
+ const moments = new Map();
165
+ for (const row of rows) {
166
+ const key = row.step === undefined ? 'none' : row.step.rank + ':' + row.step.number;
167
+ const known = moments.get(key);
168
+ if (known === undefined)
169
+ moments.set(key, { ...(row.step === undefined ? {} : { step: row.step }), count: 1 });
170
+ else
171
+ known.count += 1;
172
+ }
173
+ const at = (step) => [step?.rank ?? 9999, step?.number ?? 0];
174
+ return [...moments.values()].sort((a, b) => at(a.step)[0] - at(b.step)[0] || at(a.step)[1] - at(b.step)[1]).map(({ step, count }) => ({
175
+ group: momentWords(step),
176
+ tone: 'grey',
177
+ count,
178
+ attributes: ' data-files-step hidden data-order-agent="' + (step?.rank ?? 9999) + '" data-order-step="' + (step?.number ?? 0) + '" data-order-place="-1"',
179
+ }));
180
+ }
105
181
  /** P34: the search, the three groups with their counts, and the two questions - a script's, so only with one. */
106
- function filters(rows) {
182
+ function filters(rows, orderable) {
107
183
  const count = (test) => rows.filter(test).length;
108
184
  const groups = [['all', rows.length], ['fix', count((row) => row.group === 'fix')], ['fixed', count((row) => row.group === 'fixed')]]
109
185
  .map(([group, n]) => '<button type="button" class="fl-pill' + (group === 'all' ? ' fl-on' : '') + '" data-files-group="' + group + '" data-live-keep aria-pressed="' + String(group === 'all') + '">' +
@@ -120,13 +196,13 @@ function filters(rows) {
120
196
  '<input class="fl-search" type="search" data-files-search data-live-keep placeholder="' + e(translator('en')('fl.search')) + '"' +
121
197
  LANGS.map((lang) => ' data-placeholder-' + lang + '="' + e(translator(lang)('fl.search')) + '"').join('') + labelAttributes((t) => e(t('fl.search'))) + '>' +
122
198
  '<div class="fl-pills">' + groups + '</div></div>' +
123
- '<div class="fl-row">' + inLanguages(did) + inLanguages(prot) + textButton(inLanguages((t) => t('fl.clear')), ' data-files-clear hidden', 'fl-clear') + '</div></div>';
199
+ '<div class="fl-row">' + (orderable ? inLanguages(orderChoice) : '') + inLanguages(did) + inLanguages(prot) + textButton(inLanguages((t) => t('fl.clear')), ' data-files-clear hidden', 'fl-clear') + '</div></div>';
124
200
  }
125
201
  /**
126
202
  * P35: `File · AI · Private file · When your AI reaches it · Action` (the mode its own column since 2026-09-25; X28 names
127
203
  * the AI it holds for since 2026-09-29), a row opening its story window or its simple one (P37).
128
204
  */
129
- function table(entries, items, protectKeys, filtered, names) {
205
+ function table(entries, items, protectKeys, filtered, names, orderable) {
130
206
  const label = (key) => inLanguages((t) => t(key));
131
207
  return dataTable({
132
208
  head: [label('fl.col.file'), label('fl.col.ai'), label('fl.col.prot'), label('fl.col.mode'), label('fl.col.action')],
@@ -134,38 +210,40 @@ function table(entries, items, protectKeys, filtered, names) {
134
210
  columns: 'minmax(240px,2fr) 120px 140px minmax(250px,1.2fr) 110px',
135
211
  minWidth: 980,
136
212
  empty: label('fl.noMatch'),
137
- // Only the table of opened files is the one the filters work on; the names under it are a list, not a filter's rows.
138
- ...(filtered ? { attributes: ' data-files-table' } : {}),
139
- rows: entries.flatMap(({ row, key }, at) => {
140
- const tier = tierOf(row);
141
- // A heading over each part, where the table has more than one: its first row is where the part begins.
142
- const parts = new Set(entries.map((entry) => tierOf(entry.row))).size;
143
- const heading = parts > 1 && (at === 0 || tierOf(entries[at - 1].row) !== tier)
144
- ? [{
145
- group: label(TIERS[tier].key), tone: TIERS[tier].tone, count: entries.filter((entry) => tierOf(entry.row) === tier).length,
146
- attributes: ' data-files-tier="' + tier + '"',
147
- }]
148
- : [];
149
- const item = row.item === undefined ? undefined : items[row.item];
150
- const target = row.item === undefined ? 'file-' + key : storyId(row.item);
151
- const kind = item === undefined ? label(row.kind) : inLanguages((t, lang) => titleOf(item, t, lang));
152
- return [...heading, {
153
- cells: [
154
- '<span class="fl-kind">' + kind + '</span><span class="fl-chip" title="' + e(row.path) + '">' + e(names(row.path)) + '</span>',
155
- row.private ? statusIcon(LOOK[row.access === 'changed' ? 'read' : row.access], label('fl.acc.' + row.access)) : '<span class="fl-plain">' + label('fl.acc.' + row.access) + '</span>',
156
- privateCell(row, protectKeys.get(row.path)),
157
- modeCell(row, protectKeys.get(row.path)),
158
- actionCell(row),
159
- ],
160
- href: '#' + target,
161
- linkAttributes: opener(target),
162
- label: e(row.path),
163
- // The status bar: protected now, read before, and not fixed - the one row whose rule came too late (guidelines §4).
164
- bar: row.private && row.protection === 'yes' && row.access === 'read' && row.group === 'fix',
165
- attributes: ' data-live-key="' + e(row.path) + '" data-file-key="' + key + '" data-tier="' + tier + '" data-group="' + row.group + '" data-did="' + didOf(row) + '" data-prot="' + row.protection + '"' +
166
- ' data-search="' + e(row.path.toLowerCase()) + '"' + (row.item === undefined ? '' : ' data-file-item="' + row.item + '"'),
167
- }];
168
- }),
213
+ // Only the table of opened files is the one the filters work on; either may be put in the order the AI went (OW4).
214
+ attributes: (filtered ? ' data-files-table' : '') + (orderable ? ' data-files-orderable' : ''),
215
+ rows: [...entries.flatMap(({ row, key }, at) => {
216
+ const tier = tierOf(row);
217
+ // A heading over each part, where the table has more than one: its first row is where the part begins.
218
+ const parts = new Set(entries.map((entry) => tierOf(entry.row))).size;
219
+ const heading = parts > 1 && (at === 0 || tierOf(entries[at - 1].row) !== tier)
220
+ ? [{
221
+ group: label(TIERS[tier].key), tone: TIERS[tier].tone, count: entries.filter((entry) => tierOf(entry.row) === tier).length,
222
+ attributes: ' data-files-tier="' + tier + '"',
223
+ }]
224
+ : [];
225
+ const item = row.item === undefined ? undefined : items[row.item];
226
+ const target = row.item === undefined ? 'file-' + key : storyId(row.item);
227
+ const kind = item === undefined ? label(row.kind) : inLanguages((t, lang) => titleOf(item, t, lang));
228
+ return [...heading, {
229
+ cells: [
230
+ '<span class="fl-kind">' + kind + '</span><span class="fl-chip" title="' + e(row.path) + '">' + e(names(row.path)) + '</span>',
231
+ row.private ? statusIcon(LOOK[row.access === 'changed' ? 'read' : row.access], label('fl.acc.' + row.access)) : '<span class="fl-plain">' + label('fl.acc.' + row.access) + '</span>',
232
+ privateCell(row, protectKeys.get(row.path)),
233
+ modeCell(row, protectKeys.get(row.path)),
234
+ actionCell(row),
235
+ ],
236
+ href: '#' + target,
237
+ linkAttributes: opener(target),
238
+ label: e(row.path),
239
+ // The status bar: protected now, read before, and not fixed - the one row whose rule came too late (guidelines §4).
240
+ bar: row.private && row.protection === 'yes' && row.access === 'read' && row.group === 'fix',
241
+ attributes: ' data-live-key="' + e(row.path) + '" data-file-key="' + key + '" data-tier="' + tier + '" data-group="' + row.group + '" data-did="' + didOf(row) + '" data-prot="' + row.protection + '"' +
242
+ ' data-search="' + e(row.path.toLowerCase()) + '"' + (row.item === undefined ? '' : ' data-file-item="' + row.item + '"') +
243
+ // OW5: by AI, then step, then the file's place in it; a row with no step last.
244
+ ' data-order-agent="' + (row.step?.rank ?? 9999) + '" data-order-step="' + (row.step?.number ?? 0) + '" data-order-place="' + (row.step?.place ?? 0) + '"',
245
+ }];
246
+ }), ...(orderable ? momentHeadings(entries.map(({ row }) => row)) : [])],
169
247
  });
170
248
  }
171
249
  /**
@@ -193,7 +271,12 @@ function privateCell(row, key) {
193
271
  function modeCell(row, key) {
194
272
  const label = (id) => inLanguages((t) => t(id));
195
273
  const blocked = glyphIcon(MODE_SVG.block, 'mint', label('fl.mode.yes'));
196
- const made = key === undefined ? '' : '<span data-protected="' + key + '" hidden>' + blocked + '</span>';
274
+ const tracked = glyphIcon(MODE_SVG.tell, 'sand', label('fl.mode.told'));
275
+ // `block-or-track-from-the-report` BT7: the window writes one of two answers, so both are drawn beside the row's own
276
+ // and the script shows the one the person chose - it never writes markup of its own.
277
+ const made = key === undefined ? ''
278
+ : '<span data-made="block" data-made-key="' + key + '" hidden>' + blocked + '</span>' +
279
+ '<span data-made="tell" data-made-key="' + key + '" hidden>' + tracked + '</span>';
197
280
  if (row.protection === 'yes')
198
281
  return blocked;
199
282
  // F57: the person chose this. Nothing is wrong with it, so it is in Track's sand, and nothing offers to protect it.
@@ -230,9 +313,7 @@ function actionCell(row) {
230
313
  function fileWindow(row, key, protectKeys, names) {
231
314
  const id = 'file-' + key;
232
315
  const label = (k) => inLanguages((t) => t(k));
233
- const did = row.private
234
- ? 'fl.w.did.' + row.access
235
- : row.access === 'changed' ? 'fl.w.did.changed' : row.access === 'stopped' ? 'fl.w.did.everydayStopped' : 'fl.w.did.everydayRead';
316
+ const did = didKey(row);
236
317
  const next = nextOf(row);
237
318
  const protectKey = protectKeys.get(row.path);
238
319
  // Each answer carries its colour's glyph, so the state is seen before it is read (guidelines §9.3): what the AI did
@@ -242,12 +323,15 @@ function fileWindow(row, key, protectKeys, names) {
242
323
  [reach.glyph, row.private ? reach.tone : 'grey'],
243
324
  PROTECTED[row.protection],
244
325
  [LOOKS.none.glyph, 'grey'],
326
+ ['·', 'grey'],
245
327
  ];
246
- const fact = (at, question, answer) => {
328
+ const written = (at, question, answer) => {
247
329
  const [glyph, tone] = glyphs[at];
248
330
  return '<div class="fw-row"><span class="fw-glyph fw-' + tone + '" aria-hidden="true">' + glyph + '</span>' +
249
- '<div><div class="fw-q">' + label(question) + '</div><div class="fw-a">' + label(answer) + '</div></div></div>';
331
+ '<div><div class="fw-q">' + label(question) + '</div><div class="fw-a">' + answer + '</div></div></div>';
250
332
  };
333
+ const fact = (at, question, answer) => written(at, question, label(answer));
334
+ const moment = (at, answer) => written(at, 'fl.w.when', answer);
251
335
  return popup({
252
336
  id,
253
337
  size: 'small',
@@ -255,7 +339,10 @@ function fileWindow(row, key, protectKeys, names) {
255
339
  body: '<div class="fw"><div class="fw-top"><div class="fw-id"><h2 class="fw-title" id="' + id + '-title">' + label(row.kind) + '</h2>' +
256
340
  '<span class="fl-chip" title="' + e(row.path) + '">' + e(names(row.path)) + '</span></div>' +
257
341
  closeButton(labelAttributes((t) => t('app.close')) + CLOSES, 'sm') + '</div>' +
258
- '<div class="fw-facts">' + fact(0, 'fl.w.did', did) + fact(1, 'fl.w.prot', 'fl.w.prot.' + row.protection) + fact(2, 'fl.w.next', next) + '</div></div>' +
342
+ '<div class="fw-facts">' + fact(0, 'fl.w.did', did) + fact(1, 'fl.w.prot', 'fl.w.prot.' + row.protection) + fact(2, 'fl.w.next', next) +
343
+ // EF6: where the model says at which moment the file came up, this window says it too - the row beside it does.
344
+ // No step known draws no row: an absent answer is not the claim that nothing is known about the order (invariant 4).
345
+ (row.step === undefined ? '' : moment(3, momentWords(row.step))) + '</div></div>' +
259
346
  popupFoot('', pill({ label: label('app.close'), tone: 'outline', size: 'md', button: true, attributes: CLOSES }) +
260
347
  (protectKey === undefined ? '' : '<span data-protect-open="' + protectKey + '">' +
261
348
  pill({ label: label(protectLabel(row)), tone: row.protection === 'na' ? 'outline' : 'light', size: 'md', href: '#protect-' + protectKey, attributes: opener('protect-' + protectKey) }) + '</span>'), true),
@@ -269,6 +356,14 @@ function fileWindow(row, key, protectKeys, names) {
269
356
  */
270
357
  export const FILES_SCRIPT = String.raw `
271
358
  (() => {
359
+ // OW5 as amended 2026-10-05: in the order the AI went, each moment's heading stands over its files while one shows.
360
+ const moments = (table) => {
361
+ const by = table.classList.contains('fl-by-step');
362
+ const files = [...table.querySelectorAll('[data-file-key]')];
363
+ table.querySelectorAll('[data-files-step]').forEach((heading) => {
364
+ heading.hidden = !by || !files.some((row) => !row.hidden && row.dataset.orderAgent === heading.dataset.orderAgent && row.dataset.orderStep === heading.dataset.orderStep);
365
+ });
366
+ };
272
367
  const started = new WeakSet();
273
368
  const start = (table) => {
274
369
  if (started.has(table)) return;
@@ -288,6 +383,7 @@ export const FILES_SCRIPT = String.raw `
288
383
  table.querySelectorAll('[data-files-tier]').forEach((heading) => {
289
384
  heading.hidden = !rows.some((row) => !row.hidden && row.dataset.tier === heading.dataset.filesTier);
290
385
  });
386
+ moments(table);
291
387
  box.querySelectorAll('[data-files-shown]').forEach((element) => { element.textContent = String(shown); });
292
388
  const empty = table.querySelector('.dt-empty');
293
389
  if (empty) empty.hidden = shown !== 0;
@@ -321,7 +417,31 @@ export const FILES_SCRIPT = String.raw `
321
417
  document.addEventListener('files-changed', apply);
322
418
  apply();
323
419
  };
324
- window.agentwhyFiles = (scope) => scope.querySelectorAll('[data-files-table]').forEach(start);
420
+ // OW5, OW6: the order the AI went moves the rows and their moments' headings, by AI, step and place, and hides the
421
+ // parts' headings by a class the filters do not touch; the default puts every element back where it was drawn, the
422
+ // moments' headings hidden. Filters and search are left as they are.
423
+ const ordered = new WeakSet();
424
+ const order = (table) => {
425
+ if (ordered.has(table)) return;
426
+ ordered.add(table);
427
+ const box = table.closest('.fl') || document;
428
+ const drawn = [...table.children];
429
+ const key = (row) => [Number(row.dataset.orderAgent), Number(row.dataset.orderStep), Number(row.dataset.orderPlace)];
430
+ box.addEventListener('change', (event) => {
431
+ if (!event.target.matches('[data-files-sort]')) return;
432
+ const bySteps = event.target.value === 'steps';
433
+ box.querySelectorAll('[data-files-sort]').forEach((select) => { select.value = event.target.value; });
434
+ table.classList.toggle('fl-by-step', bySteps);
435
+ const rows = drawn.filter((element) => element.hasAttribute('data-order-agent'));
436
+ const next = bySteps ? rows.slice().sort((a, b) => { const x = key(a); const y = key(b); return x[0] - y[0] || x[1] - y[1] || x[2] - y[2]; }) : drawn;
437
+ next.forEach((element) => table.appendChild(element));
438
+ moments(table);
439
+ });
440
+ };
441
+ window.agentwhyFiles = (scope) => {
442
+ scope.querySelectorAll('[data-files-table]').forEach(start);
443
+ scope.querySelectorAll('[data-files-orderable]').forEach(order);
444
+ };
325
445
  window.agentwhyFiles(document);
326
446
  })();
327
447
  `;
@@ -334,6 +454,7 @@ export const FILES_SCRIPT = String.raw `
334
454
  */
335
455
  export const FILES_VIEW_STYLE = String.raw `
336
456
  .fl{max-width:1100px;margin:0 auto;container:files/inline-size}
457
+ .fl-by-step [data-files-tier]{display:none}
337
458
  .fl .dt-group{position:sticky;top:0;z-index:3;padding:13px 20px;font-size:16px;font-weight:650;letter-spacing:0;color:var(--text)}
338
459
  .fl .dt-group-cell{gap:10px}
339
460
  .fl .dt-group-dot{width:9px;height:9px}
@@ -359,7 +480,7 @@ export const FILES_VIEW_STYLE = String.raw `
359
480
  .fl-chip{display:inline-flex;margin-top:6px;font-family:var(--mono);font-size:13.5px;font-weight:500;color:var(--text);background:var(--white-07);border:1px solid var(--white-10);border-radius:6px;padding:2px 7px;white-space:nowrap;max-width:100%;overflow:hidden;text-overflow:ellipsis}
360
481
  .fl-plain{font-size:13.5px;font-weight:600;color:var(--text-2)}
361
482
  .fl-prot{display:flex;align-items:center;gap:10px;flex-wrap:nowrap}
362
- .fl-prot[hidden],[data-protected][hidden]{display:none}
483
+ .fl-prot[hidden],[data-protected][hidden],[data-made-key][hidden]{display:none}
363
484
  .fw-glyph svg{width:13px;height:13px}
364
485
  [data-row-control]{position:relative;z-index:2}
365
486
  .fl-act{font-size:13.5px;font-weight:600;white-space:nowrap}.fl-act[hidden]{display:none}
@@ -4,8 +4,40 @@ import { matchesGlob } from '../../../core/policy/glob.js';
4
4
  import { helperViews } from './helpers.js';
5
5
  import { nameOf, ruleKey } from './item-names.js';
6
6
  import { toDoItems } from './to-do.js';
7
+ /** Each file's step, by its path, with each AI ranked as the Helpers tab orders them: Your AI, then helpers by number. */
8
+ function stepsOf(report, accessOf) {
9
+ const helpers = [...report.graph.agents].sort((a, b) => (a.ordinal ?? Infinity) - (b.ordinal ?? Infinity) || a.index - b.index);
10
+ const agents = new Map([report.graph.main, ...helpers].map((agent, rank) => [agent.index, { rank, ordinal: agent === report.graph.main ? undefined : agent.ordinal }]));
11
+ // OW1 as amended 2026-10-05: a row stands at the first step that did to its file what the row says - the read after
12
+ // the listing that named it - else at the first that reached it. The steps come in the page's order of AIs.
13
+ const chosen = new Map();
14
+ for (const step of report.fileSteps ?? []) {
15
+ const access = accessOf.get(step.path);
16
+ const fits = (one) => access !== undefined && one.how === HOW[access];
17
+ const known = chosen.get(step.path);
18
+ if (known === undefined || (!fits(known) && fits(step)))
19
+ chosen.set(step.path, step);
20
+ }
21
+ // Each AI's moments, in its own order, numbered from 1: the action's own number stays in Advanced.
22
+ const times = new Map();
23
+ for (const step of chosen.values())
24
+ times.set(step.agentIndex, [...new Set([...(times.get(step.agentIndex) ?? []), step.step])].sort((a, b) => a - b));
25
+ return new Map([...chosen.values()].flatMap((step) => {
26
+ const agent = agents.get(step.agentIndex);
27
+ if (agent === undefined)
28
+ return [];
29
+ return [[step.path, {
30
+ rank: agent.rank, ...(agent.ordinal === undefined ? {} : { helper: agent.ordinal }),
31
+ number: (times.get(step.agentIndex) ?? []).indexOf(step.step) + 1, place: step.place,
32
+ }]];
33
+ }));
34
+ }
7
35
  const REACH = { read: 'read', unknown: 'unknown', named: 'name', stopped: 'stopped' };
8
- const STRENGTH = ['read', 'unknown', 'name', 'stopped', 'changed'];
36
+ /** What an action did to a file, in the model's words, for each thing a row says. */
37
+ const HOW = { read: 'read', name: 'named', unknown: 'unknown', stopped: 'stopped', changed: 'changed' };
38
+ // A stop outranks a name seen (changed 2026-10-05 by the maintainer): `rg --files` found it, `cat` of it was stopped -
39
+ // the rule held, which the person is to hear, and the name alone is the step before it.
40
+ const STRENGTH = ['read', 'unknown', 'stopped', 'name', 'changed'];
9
41
  export function fileRows(report, items, done, denied) {
10
42
  const patternOf = new Map(report.findings.map((finding) => [finding.path, finding.pattern]));
11
43
  const reached = new Map();
@@ -46,23 +78,34 @@ export function fileRows(report, items, done, denied) {
46
78
  const everydayRows = report.everydayFiles.filter((file) => !reached.has(file.path)).map((file) => ({
47
79
  path: file.path,
48
80
  private: false,
49
- access: file.how === 'changed' ? 'changed' : file.how === 'stopped' ? 'stopped' : 'read',
81
+ access: file.how === 'changed' ? 'changed' : file.how === 'stopped' ? 'stopped' : file.how === 'named' ? 'name' : 'read',
50
82
  protection: 'na',
51
83
  group: 'none',
52
84
  kind: kindOfName(nameOf(file.path)),
53
85
  }));
54
- return [...privateRows, ...everydayRows];
86
+ const rows = [...privateRows, ...everydayRows];
87
+ // A row's step is chosen by what the row says, so it is found once every row is.
88
+ const steps = stepsOf(report, new Map(rows.map((row) => [row.path, row.access])));
89
+ return rows.map((row) => {
90
+ const step = steps.get(row.path);
91
+ return step === undefined ? row : { ...row, step };
92
+ });
55
93
  }
56
- /** A private file the AI only saw the name of: nothing shows it opened it, or that it exists. */
94
+ /**
95
+ * A file the AI only saw the name of, private or not: nothing shows it opened it, or that it exists, so it is never
96
+ * counted as opened (P32a) - a row of the table all the same.
97
+ */
57
98
  export function onlyNamed(row) {
58
- return row.private && row.access === 'name';
99
+ return row.access === 'name';
59
100
  }
60
101
  /**
61
- * How many files the Files tab lists, as the report's sidebar counts them: every row but a name only seen. Marks and
62
- * rules change a row's group and protection, never whether it is a row, so none is needed to count them.
102
+ * How many rows the Files tab lists - what a conversation's "See all {n} files" leads to (F14). Every row, a name only
103
+ * seen among them (changed 2026-10-05: a session whose AI listed a folder had no link to the files it saw); the
104
+ * sidebar's count and the tab's heading keep to the files opened (P32a). Marks and rules change a row's group and
105
+ * protection, never whether it is a row, so none is needed to count them.
63
106
  */
64
107
  export function listedFiles(report) {
65
- return fileRows(report, toDoItems(report), new Set(), undefined).filter((row) => !onlyNamed(row)).length;
108
+ return fileRows(report, toDoItems(report), new Set(), undefined).length;
66
109
  }
67
110
  const SOURCE = /\.(ts|tsx|js|jsx|mjs|cjs|py|go|rb|rs|java|kt|swift|c|h|cpp|hpp|cs|php|sh|vue|svelte|css|scss|html|sql)$/i;
68
111
  /** P33's fixed table: what an everyday file is, from its name alone. */
@@ -46,30 +46,40 @@ export const FIX_WIZARD_SCRIPT = String.raw `
46
46
  };
47
47
 
48
48
  // P38: the rule is sent, and the window closes only once it is written; otherwise it says why, or what to run.
49
+ // BT6: the window may ask which mode the file gets, and each of its two buttons says which it is. A block is the deny
50
+ // rule it has always been; Track takes no rule out - there is none holding the file - and puts the pattern on this
51
+ // person's own told list, where a block written from here already goes without asking (BTD2).
49
52
  const protect = (button) => {
50
53
  const dialog = button.closest('dialog');
51
- const every = dialog.querySelector('[data-protect-every]');
54
+ const mode = button.getAttribute('data-protect-mode') || 'block';
55
+ const every = dialog.querySelector('[data-protect-every="' + mode + '"]');
52
56
  const pattern = every && every.checked ? button.getAttribute('data-pattern-every') : button.getAttribute('data-pattern');
53
57
  const reason = dialog.querySelector('[data-note-reason]');
54
- const command = dialog.querySelector('[data-note-command]');
58
+ const command = dialog.querySelector('[data-note-command="' + mode + '"]');
55
59
  reason.hidden = true;
56
- command.hidden = true;
60
+ if (command) command.hidden = true;
57
61
  const buttons = [...dialog.querySelectorAll('button')];
58
62
  buttons.forEach((each) => { each.disabled = true; });
59
- send('api/settings', { change: 'protect', pattern }).then((answer) => {
63
+ const body = mode === 'tell'
64
+ ? { change: 'mode', to: 'tell', patterns: [pattern], rules: [], where: 'local' }
65
+ : { change: 'protect', pattern };
66
+ send('api/settings', body).then((answer) => {
60
67
  buttons.forEach((each) => { each.disabled = false; });
61
68
  if (answer.ok) {
62
69
  const at = button.getAttribute('data-protect');
63
70
  document.querySelectorAll('[data-protect-open="' + at + '"]').forEach((open) => { open.hidden = true; open.style.display = 'none'; });
64
71
  document.querySelectorAll('[data-protected="' + at + '"]').forEach((tag) => { tag.hidden = false; });
65
- document.querySelectorAll('[data-file-key="' + at + '"]').forEach((row) => { row.dataset.prot = 'yes'; });
72
+ // BT7: the answer the person chose, out of the ones the row was drawn with.
73
+ document.querySelectorAll('[data-made-key="' + at + '"]').forEach((one) => { one.hidden = one.getAttribute('data-made') !== mode; });
74
+ document.querySelectorAll('[data-file-key="' + at + '"]').forEach((row) => { row.dataset.prot = mode === 'tell' ? 'told' : 'yes'; });
66
75
  document.dispatchEvent(new CustomEvent('files-changed'));
67
76
  dialog.close();
68
77
  } else if (answer.reached) {
69
78
  reason.textContent = answer.message;
70
79
  reason.hidden = false;
71
- } else {
72
- handOver(command, 'agentwhy init --protect ' + quoted(pattern));
80
+ } else if (command) {
81
+ // BT8: no flag writes a told list, so Track hands over the command that opens the page where it can be written.
82
+ handOver(command, mode === 'tell' ? 'agentwhy start' : 'agentwhy init --protect ' + quoted(pattern));
73
83
  }
74
84
  });
75
85
  };
@@ -165,7 +165,9 @@ function dataSteps(item, page, note) {
165
165
  : key === undefined
166
166
  ? '<p class="wz-p">' + label(page.shared ? 'wz.stop.shared' : 'wz.stop.outside') + '</p>'
167
167
  : pill({ label: label('wz.protect'), tone: 'light', size: 'lg', href: '#protect-' + key, attributes: opener('protect-' + key) + ' data-protect-open="' + key + '"' }) +
168
- '<span class="wz-protected" data-protected="' + key + '" hidden>' + label('pr.done') + '</span>' +
168
+ // BT7: the window writes one of two answers, so the step holds both and the script shows the one chosen.
169
+ '<span class="wz-protected" data-made="block" data-made-key="' + key + '" hidden>' + label('pr.done') + '</span>' +
170
+ '<span class="wz-protected wz-tracked" data-made="tell" data-made-key="' + key + '" hidden>' + label('md.done.tell') + '</span>' +
169
171
  '<p class="wz-honest">' + label('wz.stop.honest') + '</p>';
170
172
  return '<section class="wz-step" data-step="1">' + stepHead('wz.stepOf', 1, 'wz.data.title') +
171
173
  '<div class="wz-ask">' + label('wz.data.ask') + '</div>' +
@@ -284,6 +286,7 @@ export const FIX_WIZARD_STYLE = String.raw `
284
286
  .wz-reason{margin:18px 0 0;padding:12px 14px;border-radius:12px;background:var(--coral-07);border:1px solid var(--coral-35);font-size:14px;line-height:1.5;white-space:pre-line}
285
287
  .wz-reason[hidden],.wz-handover[hidden],.wz-protected[hidden]{display:none}
286
288
  .wz-protected{display:inline-flex;align-items:center;padding:12px 20px;border-radius:999px;background:var(--mint-14);color:var(--mint);font-size:15px;font-weight:600}
289
+ .wz-tracked{background:var(--sand-16);color:var(--sand)}
287
290
  .wz-foot{position:sticky;bottom:0;z-index:1;display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap;padding:16px 24px;border-top:1px solid var(--white-07);background:var(--popup-foot)}
288
291
  .wz-foot-left,.wz-foot-right{display:flex;gap:14px;align-items:center}.wz-foot-right{gap:10px}
289
292
  .wz-back{background:transparent;border:1px solid var(--white-12);color:var(--text-soft);border-radius:999px;padding:10px 16px;font:inherit;font-size:14px;cursor:pointer}
@@ -1,6 +1,7 @@
1
1
  import { isTemplate } from '../../check/session-actions.js';
2
2
  import { entryOf, storyAgents } from './file-story.js';
3
- const RANK = ['read', 'unknown', 'named', 'stopped'];
3
+ // A stop outranks a name seen, as a file's access does (`files.ts`, 2026-10-05).
4
+ const RANK = ['read', 'unknown', 'stopped', 'named'];
4
5
  export function helperViews(report) {
5
6
  const agentOf = storyAgents(report);
6
7
  const helpers = [...report.graph.agents].sort((a, b) => (a.ordinal ?? Infinity) - (b.ordinal ?? Infinity) || a.index - b.index);