kushi-agents 6.1.2 → 6.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/plugin/instructions/agentskills-compliance.instructions.md +144 -0
- package/plugin/instructions/dashboard-artifact.instructions.md +132 -0
- package/plugin/instructions/guided-tour.instructions.md +100 -0
- package/plugin/instructions/karpathy-state-layout.instructions.md +124 -0
- package/plugin/instructions/schema-evolve.instructions.md +73 -0
- package/plugin/instructions/skill-authoring.instructions.md +147 -0
- package/plugin/instructions/skill-evals.instructions.md +130 -0
- package/plugin/runners/bootstrap.mjs +55 -22
- package/plugin/runners/lib/runlog.mjs +153 -6
- package/plugin/runners/migrate-to-v550.mjs +192 -0
- package/plugin/runners/pull-email.mjs +194 -3
- package/plugin/runners/pull-meetings.mjs +207 -4
- package/plugin/runners/pull-onenote.mjs +239 -3
- package/plugin/runners/pull-sharepoint.mjs +284 -3
- package/plugin/runners/pull-state.mjs +297 -0
- package/plugin/runners/pull-teams.mjs +170 -3
- package/plugin/runners/refresh.mjs +9 -1
- package/plugin/runners/test/fixtures/email-abn-amro.json +13 -0
- package/plugin/runners/test/fixtures/email-novel-error.json +9 -0
- package/plugin/runners/test/fixtures/meetings-abn-amro.json +10 -0
- package/plugin/runners/test/fixtures/meetings-body-unavailable.json +10 -0
- package/plugin/runners/test/fixtures/onenote-abn-amro.json +30 -0
- package/plugin/runners/test/fixtures/onenote-partial.json +21 -0
- package/plugin/runners/test/fixtures/refresh-dir/email.json +7 -4
- package/plugin/runners/test/fixtures/refresh-dir/teams.json +6 -4
- package/plugin/runners/test/fixtures/sharepoint-abn-amro.json +12 -0
- package/plugin/runners/test/fixtures/teams-abn-amro.json +11 -0
- package/plugin/runners/test/integration/migrate-to-v550.integration.test.mjs +138 -0
- package/plugin/runners/test/integration/pull-email.integration.test.mjs +149 -0
- package/plugin/runners/test/integration/pull-meetings.integration.test.mjs +92 -0
- package/plugin/runners/test/integration/pull-onenote.integration.test.mjs +86 -0
- package/plugin/runners/test/integration/pull-sharepoint.integration.test.mjs +93 -0
- package/plugin/runners/test/integration/pull-teams.integration.test.mjs +91 -0
- package/plugin/runners/test/unit/runlog.test.mjs +1 -1
- package/plugin/skills/build-state/SKILL.md +195 -0
- package/plugin/skills/build-state/evals/evals.json +31 -0
- package/plugin/skills/dashboard/SKILL.md +132 -0
- package/plugin/skills/dashboard/evals/evals.json +33 -0
- package/plugin/skills/lint-state/.created-by-skill-creator +0 -0
- package/plugin/skills/lint-state/SKILL.md +98 -0
- package/plugin/skills/lint-state/evals/evals.json +34 -0
- package/plugin/skills/lint-state/lint.ps1 +218 -0
- package/plugin/skills/promote/.created-by-skill-creator +1 -0
- package/plugin/skills/promote/SKILL.md +125 -0
- package/plugin/skills/promote/evals/evals.json +35 -0
- package/plugin/skills/schema-evolve/.created-by-skill-creator +0 -0
- package/plugin/skills/schema-evolve/SKILL.md +106 -0
- package/plugin/skills/schema-evolve/evals/evals.json +37 -0
- package/plugin/skills/skill-checker/SKILL.md +136 -0
- package/plugin/skills/skill-checker/check-skill.ps1 +416 -0
- package/plugin/skills/skill-checker/evals/evals.json +41 -0
- package/plugin/skills/skill-creator/SKILL.md +134 -0
- package/plugin/skills/skill-creator/evals/evals.json +40 -0
- package/plugin/skills/skill-creator/generate-eval-review.ps1 +101 -0
- package/plugin/skills/skill-creator/optimize-description.ps1 +87 -0
- package/plugin/skills/skill-creator/scaffold.ps1 +180 -0
- package/plugin/skills/skill-creator/templates/evals-starter.template.json +27 -0
- package/plugin/skills/skill-creator/templates/gotchas-stub.template.md +9 -0
- package/plugin/skills/skill-creator/templates/skill-skeleton.template.md +28 -0
- package/plugin/skills/teach/.created-by-skill-creator +0 -0
- package/plugin/skills/teach/SKILL.md +79 -0
- package/plugin/skills/teach/evals/evals.json +59 -0
- package/plugin/skills/tour/SKILL.md +85 -0
- package/plugin/skills/tour/build-tour.ps1 +185 -0
- package/plugin/skills/tour/evals/evals.json +33 -0
- package/plugin/templates/state/00_overview.template.md +44 -0
- package/plugin/templates/state/01_decisions.template.md +41 -0
- package/plugin/templates/state/02_stakeholders.template.md +48 -0
- package/plugin/templates/state/03_architecture-and-solution.template.md +56 -0
- package/plugin/templates/state/04_workshops-and-key-meetings.template.md +43 -0
- package/plugin/templates/state/05_action-items.template.md +29 -0
- package/plugin/templates/state/06_risks-and-issues.template.md +43 -0
- package/plugin/templates/state/07_timeline-and-milestones.template.md +45 -0
- package/plugin/templates/state/08_artifacts-and-deliverables.template.md +55 -0
- package/plugin/templates/state/09_open-questions.template.md +62 -0
- package/plugin/templates/state/AGENTS.template.md +33 -0
- package/plugin/templates/state/CLAUDE.template.md +33 -0
- package/plugin/templates/state/README.md +41 -0
- package/plugin/templates/state/answers.README.md +7 -0
- package/plugin/templates/state/hot.template.md +12 -0
- package/plugin/templates/state/index.template.md +41 -0
- package/plugin/templates/state/log.template.md +14 -0
- package/plugin/templates/state/page.template.md +22 -0
- package/plugin/templates/state/review-queue.template.md +10 -0
- package/plugin/runners/test/integration/csc-pull.integration.test.mjs +0 -160
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// plugin/runners/pull-state.mjs
|
|
3
|
+
// Deterministic State/ generator. Inventories Evidence/ and produces:
|
|
4
|
+
// State/index.md — TOC pointing at every evidence file by source/week
|
|
5
|
+
// State/log.md — chronological run + evidence ledger
|
|
6
|
+
// State/CLAUDE.md — host-agnostic project context (project name, sources, alias inventory)
|
|
7
|
+
// State/AGENTS.md — alias of CLAUDE.md for OpenAI-flavored hosts
|
|
8
|
+
//
|
|
9
|
+
// This runner does NOT do narrative synthesis — that is the build-state LLM
|
|
10
|
+
// skill's job. This produces the structural skeleton that makes the LLM
|
|
11
|
+
// skill's work cheaper and reproducible. v5.9.0.
|
|
12
|
+
//
|
|
13
|
+
// Usage:
|
|
14
|
+
// node plugin/runners/pull-state.mjs --project <P> [--dry-run] [--include-legacy]
|
|
15
|
+
|
|
16
|
+
import path from 'node:path';
|
|
17
|
+
import { promises as fs } from 'node:fs';
|
|
18
|
+
import YAML from 'yaml';
|
|
19
|
+
import { evidenceRoot, projectRoot, sharedRoot } from './lib/layout.mjs';
|
|
20
|
+
import { writeAtomic, pathExists } from './lib/evidence.mjs';
|
|
21
|
+
|
|
22
|
+
function parseArgs(argv) {
|
|
23
|
+
const args = { dryRun: false, includeLegacy: true };
|
|
24
|
+
for (let i = 0; i < argv.length; i++) {
|
|
25
|
+
const a = argv[i];
|
|
26
|
+
if (a === '--project') args.project = argv[++i];
|
|
27
|
+
else if (a === '--dry-run') args.dryRun = true;
|
|
28
|
+
else if (a === '--no-legacy') args.includeLegacy = false;
|
|
29
|
+
else if (a === '--help' || a === '-h') args.help = true;
|
|
30
|
+
}
|
|
31
|
+
return args;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function help() {
|
|
35
|
+
return `Usage: node pull-state.mjs --project <P> [--dry-run] [--no-legacy]`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function emit(obj) { process.stdout.write(JSON.stringify(obj) + '\n'); }
|
|
39
|
+
function log(msg) { process.stderr.write(`[state] ${msg}\n`); }
|
|
40
|
+
|
|
41
|
+
const SOURCES = ['email', 'teams', 'meetings', 'onenote', 'sharepoint', 'crm', 'ado'];
|
|
42
|
+
|
|
43
|
+
async function listDirs(p) {
|
|
44
|
+
try {
|
|
45
|
+
return (await fs.readdir(p, { withFileTypes: true })).filter(e => e.isDirectory()).map(e => e.name);
|
|
46
|
+
} catch { return []; }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function listFiles(p, exts = ['.md', '.yml']) {
|
|
50
|
+
try {
|
|
51
|
+
return (await fs.readdir(p, { withFileTypes: true }))
|
|
52
|
+
.filter(e => e.isFile() && exts.includes(path.extname(e.name).toLowerCase()))
|
|
53
|
+
.map(e => e.name);
|
|
54
|
+
} catch { return []; }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Inventory one alias's source folder. Returns { weekly, snapshot, stream, index, total }. */
|
|
58
|
+
async function inventoryAliasSource(aliasSourceDir) {
|
|
59
|
+
const result = { weekly: [], snapshot: [], stream: [], index: [], total: 0 };
|
|
60
|
+
for (const layout of ['weekly', 'snapshot', 'stream', '_index']) {
|
|
61
|
+
const dir = path.join(aliasSourceDir, layout);
|
|
62
|
+
const files = await listFiles(dir);
|
|
63
|
+
const key = layout === '_index' ? 'index' : layout;
|
|
64
|
+
result[key] = files.map(f => path.join(layout, f)).sort();
|
|
65
|
+
result.total += files.length;
|
|
66
|
+
}
|
|
67
|
+
return result;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Inventory shared sources (crm, ado, references). */
|
|
71
|
+
async function inventoryShared(project) {
|
|
72
|
+
const out = {};
|
|
73
|
+
const shared = sharedRoot(project);
|
|
74
|
+
for (const sub of ['crm', 'ado', 'references']) {
|
|
75
|
+
const dir = path.join(shared, sub);
|
|
76
|
+
if (!await pathExists(dir)) continue;
|
|
77
|
+
out[sub] = await listFilesRecursive(dir, shared);
|
|
78
|
+
}
|
|
79
|
+
return out;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function listFilesRecursive(dir, base) {
|
|
83
|
+
const out = [];
|
|
84
|
+
async function walk(d) {
|
|
85
|
+
let entries;
|
|
86
|
+
try { entries = await fs.readdir(d, { withFileTypes: true }); } catch { return; }
|
|
87
|
+
for (const e of entries) {
|
|
88
|
+
const full = path.join(d, e.name);
|
|
89
|
+
if (e.isDirectory()) await walk(full);
|
|
90
|
+
else if (e.isFile() && ['.md','.yml'].includes(path.extname(e.name).toLowerCase())) {
|
|
91
|
+
out.push(path.relative(base, full).replaceAll('\\','/'));
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
await walk(dir);
|
|
96
|
+
return out.sort();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function readIntegrations(project) {
|
|
100
|
+
const p = path.join(projectRoot(project), 'integrations.yml');
|
|
101
|
+
if (!await pathExists(p)) return {};
|
|
102
|
+
try { return YAML.parse(await fs.readFile(p, 'utf8')) || {}; } catch { return {}; }
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function readBoundaries(project, alias) {
|
|
106
|
+
const p = path.join(evidenceRoot(project), alias, 'boundaries.yml');
|
|
107
|
+
if (!await pathExists(p)) return {};
|
|
108
|
+
try { return YAML.parse(await fs.readFile(p, 'utf8')) || {}; } catch { return {}; }
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function fmtSection(title, lines) {
|
|
112
|
+
return [`## ${title}`, '', ...lines, ''].join('\n');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function buildIndex({ projectName, integrations, aliases, shared, generatedAt }) {
|
|
116
|
+
const lines = [];
|
|
117
|
+
lines.push(`# ${projectName} — State Index`);
|
|
118
|
+
lines.push('');
|
|
119
|
+
lines.push(`Generated by \`pull-state.mjs\` on ${generatedAt}.`);
|
|
120
|
+
lines.push('');
|
|
121
|
+
lines.push('Mechanical inventory only. For narrative synthesis, use the `build-state` skill.');
|
|
122
|
+
lines.push('');
|
|
123
|
+
|
|
124
|
+
const integLines = [];
|
|
125
|
+
if (integrations.crm?.request_id) integLines.push(`- **CRM**: \`${integrations.crm.request_id}\``);
|
|
126
|
+
if (integrations.ado?.engagement_id) integLines.push(`- **ADO**: \`${integrations.ado.engagement_id}\``);
|
|
127
|
+
const spSites = integrations.sharepoint?.sites || [];
|
|
128
|
+
if (spSites.length) {
|
|
129
|
+
integLines.push(`- **SharePoint sites** (${spSites.length}):`);
|
|
130
|
+
for (const s of spSites.slice(0, 10)) integLines.push(` - ${s}`);
|
|
131
|
+
}
|
|
132
|
+
if (integLines.length) lines.push(fmtSection('Integrations (project-shared)', integLines));
|
|
133
|
+
|
|
134
|
+
// Shared evidence
|
|
135
|
+
const sharedLines = [];
|
|
136
|
+
for (const [src, files] of Object.entries(shared)) {
|
|
137
|
+
if (!files.length) continue;
|
|
138
|
+
sharedLines.push(`### ${src} (${files.length} file${files.length === 1 ? '' : 's'})`);
|
|
139
|
+
sharedLines.push('');
|
|
140
|
+
for (const f of files.slice(0, 30)) sharedLines.push(`- \`Evidence/_shared/${f}\``);
|
|
141
|
+
if (files.length > 30) sharedLines.push(`- _… and ${files.length - 30} more_`);
|
|
142
|
+
sharedLines.push('');
|
|
143
|
+
}
|
|
144
|
+
if (sharedLines.length) lines.push(fmtSection('Shared Evidence', sharedLines));
|
|
145
|
+
|
|
146
|
+
// Per-alias
|
|
147
|
+
for (const a of aliases) {
|
|
148
|
+
const aliasLines = [];
|
|
149
|
+
aliasLines.push(`Boundaries:`);
|
|
150
|
+
for (const [k, v] of Object.entries(a.boundaries || {})) {
|
|
151
|
+
const arr = Array.isArray(v) ? v : (v?.folders || v?.chats || v?.joinUrls || v?.section_file_ids || v?.sites || []);
|
|
152
|
+
if (Array.isArray(arr) && arr.length) aliasLines.push(` - ${k}: ${arr.length} item(s)`);
|
|
153
|
+
}
|
|
154
|
+
aliasLines.push('');
|
|
155
|
+
for (const [src, inv] of Object.entries(a.sources || {})) {
|
|
156
|
+
if (inv.total === 0) continue;
|
|
157
|
+
aliasLines.push(`**${src}** — ${inv.total} file(s) (weekly: ${inv.weekly.length}, snapshot: ${inv.snapshot.length}, stream: ${inv.stream.length}, index: ${inv.index.length})`);
|
|
158
|
+
const all = [...inv.weekly, ...inv.snapshot, ...inv.stream, ...inv.index];
|
|
159
|
+
for (const f of all.slice(0, 12)) aliasLines.push(`- \`Evidence/${a.alias}/${src}/${f}\``);
|
|
160
|
+
if (all.length > 12) aliasLines.push(`- _… and ${all.length - 12} more_`);
|
|
161
|
+
aliasLines.push('');
|
|
162
|
+
}
|
|
163
|
+
lines.push(fmtSection(`Contributor: ${a.alias}`, aliasLines));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return lines.join('\n').replace(/\n{3,}/g, '\n\n');
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function buildLog({ projectName, runLog, generatedAt }) {
|
|
170
|
+
const lines = [];
|
|
171
|
+
lines.push(`# ${projectName} — Run Log`);
|
|
172
|
+
lines.push('');
|
|
173
|
+
lines.push(`Generated by \`pull-state.mjs\` on ${generatedAt}. Reflects \`Evidence/run-log.yml\`.`);
|
|
174
|
+
lines.push('');
|
|
175
|
+
if (!runLog || !Array.isArray(runLog.entries) || runLog.entries.length === 0) {
|
|
176
|
+
lines.push('_No run-log entries yet._');
|
|
177
|
+
return lines.join('\n');
|
|
178
|
+
}
|
|
179
|
+
const entries = [...runLog.entries].sort((a, b) => String(b.timestamp || '').localeCompare(String(a.timestamp || '')));
|
|
180
|
+
for (const e of entries.slice(0, 100)) {
|
|
181
|
+
const ts = e.timestamp || '?';
|
|
182
|
+
const status = e.status || '?';
|
|
183
|
+
const src = e.source || '?';
|
|
184
|
+
const ent = e.entity ? ` \`${e.entity}\`` : '';
|
|
185
|
+
const wk = e.week ? ` (week ${e.week})` : '';
|
|
186
|
+
lines.push(`- **${ts}** — ${src}${ent}${wk} → \`${status}\``);
|
|
187
|
+
}
|
|
188
|
+
if (entries.length > 100) lines.push(`\n_Showing 100 of ${entries.length} entries._`);
|
|
189
|
+
return lines.join('\n');
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function buildClaude({ projectName, integrations, aliases, shared }) {
|
|
193
|
+
const lines = [];
|
|
194
|
+
lines.push(`# ${projectName} — Project Context`);
|
|
195
|
+
lines.push('');
|
|
196
|
+
lines.push('Auto-generated by `pull-state.mjs`. This file gives any AI agent (Claude, Copilot, etc.) the minimal facts to be useful in this engagement.');
|
|
197
|
+
lines.push('');
|
|
198
|
+
lines.push('## Project');
|
|
199
|
+
lines.push(`- Name: \`${projectName}\``);
|
|
200
|
+
if (integrations.crm?.request_id) lines.push(`- CRM request: \`${integrations.crm.request_id}\``);
|
|
201
|
+
if (integrations.ado?.engagement_id) lines.push(`- ADO engagement: \`${integrations.ado.engagement_id}\``);
|
|
202
|
+
if (integrations.sharepoint?.sites?.length) {
|
|
203
|
+
lines.push(`- SharePoint sites: ${integrations.sharepoint.sites.length}`);
|
|
204
|
+
}
|
|
205
|
+
lines.push('');
|
|
206
|
+
lines.push('## Contributors');
|
|
207
|
+
for (const a of aliases) {
|
|
208
|
+
const totalFiles = Object.values(a.sources || {}).reduce((s, inv) => s + (inv.total || 0), 0);
|
|
209
|
+
lines.push(`- \`${a.alias}\`: ${totalFiles} evidence file(s)`);
|
|
210
|
+
}
|
|
211
|
+
lines.push('');
|
|
212
|
+
lines.push('## Where things live');
|
|
213
|
+
lines.push('- Per-contributor evidence: `Evidence/<alias>/<source>/...`');
|
|
214
|
+
lines.push('- Shared evidence: `Evidence/_shared/{crm,ado,references}/`');
|
|
215
|
+
lines.push('- Project-wide config: `integrations.yml`');
|
|
216
|
+
lines.push('- This index: `State/index.md`, `State/log.md`');
|
|
217
|
+
lines.push('');
|
|
218
|
+
lines.push('## Doctrine');
|
|
219
|
+
lines.push('- Cite every claim. Use the form `[source: <relative-path> · <iso-ts>]`.');
|
|
220
|
+
lines.push('- Read-only Q&A: see the `ask-project` skill.');
|
|
221
|
+
lines.push('- Refresh + state regen: `kushi refresh <project>` then `kushi state <project>`.');
|
|
222
|
+
return lines.join('\n');
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
async function main() {
|
|
226
|
+
const args = parseArgs(process.argv.slice(2));
|
|
227
|
+
if (args.help) { console.log(help()); return 0; }
|
|
228
|
+
if (!args.project) { console.error(help()); emit({ status: 'failed', error: 'required: --project' }); return 2; }
|
|
229
|
+
|
|
230
|
+
const root = projectRoot(args.project);
|
|
231
|
+
if (!await pathExists(root)) { emit({ status: 'failed', error: `project-not-bootstrapped: ${root}` }); return 2; }
|
|
232
|
+
const evRoot = evidenceRoot(root);
|
|
233
|
+
if (!await pathExists(evRoot)) { emit({ status: 'failed', error: `evidence-missing: ${evRoot}` }); return 2; }
|
|
234
|
+
|
|
235
|
+
const projectName = path.basename(root);
|
|
236
|
+
const generatedAt = new Date().toISOString();
|
|
237
|
+
|
|
238
|
+
log(`scanning ${evRoot}...`);
|
|
239
|
+
const integrations = await readIntegrations(root);
|
|
240
|
+
const shared = await inventoryShared(root);
|
|
241
|
+
|
|
242
|
+
// Per-alias inventory
|
|
243
|
+
const dirs = await listDirs(evRoot);
|
|
244
|
+
const aliasNames = dirs.filter(d => !d.startsWith('_'));
|
|
245
|
+
const aliases = [];
|
|
246
|
+
for (const alias of aliasNames) {
|
|
247
|
+
const aliasDir = path.join(evRoot, alias);
|
|
248
|
+
const sources = {};
|
|
249
|
+
for (const src of SOURCES) {
|
|
250
|
+
const srcDir = path.join(aliasDir, src);
|
|
251
|
+
if (!await pathExists(srcDir)) { sources[src] = { weekly:[], snapshot:[], stream:[], index:[], total:0 }; continue; }
|
|
252
|
+
sources[src] = await inventoryAliasSource(srcDir);
|
|
253
|
+
}
|
|
254
|
+
aliases.push({ alias, boundaries: await readBoundaries(root, alias), sources });
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// run-log.yml
|
|
258
|
+
let runLog = {};
|
|
259
|
+
const runLogPath = path.join(evRoot, 'run-log.yml');
|
|
260
|
+
if (await pathExists(runLogPath)) {
|
|
261
|
+
try { runLog = YAML.parse(await fs.readFile(runLogPath, 'utf8')) || {}; } catch { runLog = {}; }
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const stateDir = path.join(root, 'State');
|
|
265
|
+
const indexMd = buildIndex({ projectName, integrations, aliases, shared, generatedAt });
|
|
266
|
+
const logMd = buildLog({ projectName, runLog, generatedAt });
|
|
267
|
+
const claudeMd = buildClaude({ projectName, integrations, aliases, shared });
|
|
268
|
+
|
|
269
|
+
const writes = [];
|
|
270
|
+
if (!args.dryRun) {
|
|
271
|
+
await fs.mkdir(stateDir, { recursive: true });
|
|
272
|
+
const r1 = await writeAtomic(path.join(stateDir, 'index.md'), indexMd);
|
|
273
|
+
const r2 = await writeAtomic(path.join(stateDir, 'log.md'), logMd);
|
|
274
|
+
const r3 = await writeAtomic(path.join(stateDir, 'CLAUDE.md'), claudeMd);
|
|
275
|
+
const r4 = await writeAtomic(path.join(stateDir, 'AGENTS.md'), claudeMd);
|
|
276
|
+
writes.push(r1, r2, r3, r4);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
log(`done: ${aliases.length} contributor(s), ${SOURCES.length} sources scanned`);
|
|
280
|
+
emit({
|
|
281
|
+
status: 'ok',
|
|
282
|
+
project: root,
|
|
283
|
+
project_name: projectName,
|
|
284
|
+
dry_run: args.dryRun,
|
|
285
|
+
contributors: aliases.length,
|
|
286
|
+
contributors_list: aliases.map(a => a.alias),
|
|
287
|
+
state_dir: path.relative(root, stateDir).replaceAll('\\', '/'),
|
|
288
|
+
files_written: writes.filter(w => w?.written).map(w => path.relative(root, w.path).replaceAll('\\','/')),
|
|
289
|
+
note: 'Mechanical inventory only. Run the build-state LLM skill for narrative synthesis.',
|
|
290
|
+
});
|
|
291
|
+
return 0;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
main().then(c => process.exit(c || 0)).catch(e => {
|
|
295
|
+
emit({ status: 'failed', error: e.message || String(e) });
|
|
296
|
+
process.exit(1);
|
|
297
|
+
});
|
|
@@ -1,7 +1,174 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// plugin/runners/pull-teams.mjs
|
|
3
|
-
// Deterministic
|
|
3
|
+
// Deterministic Teams chat pull: one chat × one ISO week per call.
|
|
4
|
+
// Writes to Evidence/<alias>/teams/<chat-hash>/<week>/.
|
|
5
|
+
//
|
|
6
|
+
// Usage:
|
|
7
|
+
// node plugin/runners/pull-teams.mjs --project <P> --alias <A> --entity <chat-id>
|
|
8
|
+
// [--week YYYY-MM-DD] [--dry-run] [--force] [--fixture <path>]
|
|
4
9
|
|
|
5
|
-
import
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import { promises as fs } from 'node:fs';
|
|
12
|
+
import YAML from 'yaml';
|
|
13
|
+
import { assertProject, loadConfig } from './lib/config.mjs';
|
|
14
|
+
import { sourceDir } from './lib/layout.mjs';
|
|
15
|
+
import { writeAtomic } from './lib/evidence.mjs';
|
|
16
|
+
import { fetchAllPages, encodeODataOp } from './lib/http.mjs';
|
|
17
|
+
import { getToken, SCOPES } from './lib/identity.mjs';
|
|
18
|
+
import { updateCell } from './lib/ledger.mjs';
|
|
19
|
+
import { appendRunLog } from './lib/runlog.mjs';
|
|
20
|
+
import { enqueue, clear } from './lib/deferred.mjs';
|
|
21
|
+
import { shortHash } from './lib/dedup.mjs';
|
|
22
|
+
import { currentIsoMonday, ymd, parseYmd } from './lib/weeks.mjs';
|
|
23
|
+
import { emitLearningCandidate } from './lib/learnings.mjs';
|
|
6
24
|
|
|
7
|
-
|
|
25
|
+
const SOURCE = 'teams';
|
|
26
|
+
|
|
27
|
+
function parseArgs(argv) {
|
|
28
|
+
const args = { dryRun: false, force: false };
|
|
29
|
+
for (let i = 0; i < argv.length; i++) {
|
|
30
|
+
const a = argv[i];
|
|
31
|
+
if (a === '--project') args.project = argv[++i];
|
|
32
|
+
else if (a === '--alias') args.alias = argv[++i];
|
|
33
|
+
else if (a === '--entity') args.entity = argv[++i];
|
|
34
|
+
else if (a === '--week') args.week = argv[++i];
|
|
35
|
+
else if (a === '--dry-run') args.dryRun = true;
|
|
36
|
+
else if (a === '--force') args.force = true;
|
|
37
|
+
else if (a === '--fixture') args.fixture = argv[++i];
|
|
38
|
+
else if (a === '--help' || a === '-h') args.help = true;
|
|
39
|
+
}
|
|
40
|
+
return args;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function help() {
|
|
44
|
+
return `Usage: node pull-teams.mjs --project <P> --alias <A> --entity <chat-id>
|
|
45
|
+
[--week YYYY-MM-DD] [--dry-run] [--force] [--fixture <path>]`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function emit(obj) { process.stdout.write(JSON.stringify(obj) + '\n'); }
|
|
49
|
+
function configError(msg) { const e = new Error(msg); e.exitCode = 2; return e; }
|
|
50
|
+
function authError(msg) { const e = new Error(msg); e.exitCode = 3; return e; }
|
|
51
|
+
|
|
52
|
+
async function buildClient({ fixture }) {
|
|
53
|
+
if (fixture) {
|
|
54
|
+
const data = JSON.parse(await fs.readFile(fixture, 'utf8'));
|
|
55
|
+
return makeFixtureClient(data);
|
|
56
|
+
}
|
|
57
|
+
const token = await getToken(SCOPES.graph).catch(e => { throw authError(`token fetch failed: ${e.message}`); });
|
|
58
|
+
const headers = { Authorization: `Bearer ${token}`, Accept: 'application/json' };
|
|
59
|
+
return {
|
|
60
|
+
async listMessages(chatId, fromIso, toIso) {
|
|
61
|
+
// Graph chats messages: server filters createdDateTime; some tenants ignore. We re-filter locally.
|
|
62
|
+
const filter = `createdDateTime ge ${fromIso} and createdDateTime lt ${toIso}`;
|
|
63
|
+
const url = `https://graph.microsoft.com/v1.0/chats/${encodeURIComponent(chatId)}/messages?${encodeODataOp('$filter')}=${encodeURIComponent(filter)}&${encodeODataOp('$orderby')}=createdDateTime asc&${encodeODataOp('$top')}=50`;
|
|
64
|
+
const { items } = await fetchAllPages(url, { headers });
|
|
65
|
+
return items.filter(m => m.createdDateTime >= fromIso && m.createdDateTime < toIso);
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function makeFixtureClient(data) {
|
|
71
|
+
return {
|
|
72
|
+
async listMessages(chatId, fromIso, toIso) {
|
|
73
|
+
const all = (data.messagesByChat && data.messagesByChat[chatId]) || [];
|
|
74
|
+
return all.filter(m => m.createdDateTime >= fromIso && m.createdDateTime < toIso);
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function weekBounds(weekStartYmd) {
|
|
80
|
+
const start = parseYmd(weekStartYmd);
|
|
81
|
+
const end = new Date(start);
|
|
82
|
+
end.setDate(end.getDate() + 7);
|
|
83
|
+
return { fromIso: start.toISOString(), toIso: end.toISOString() };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function main() {
|
|
87
|
+
const args = parseArgs(process.argv.slice(2));
|
|
88
|
+
if (args.help) { console.log(help()); return 0; }
|
|
89
|
+
if (!args.project || !args.alias || !args.entity) {
|
|
90
|
+
console.error(help());
|
|
91
|
+
emit({ source: SOURCE, status: 'failed', errors: [{ signature: 'bad-args', message: 'required: --project --alias --entity' }] });
|
|
92
|
+
return 2;
|
|
93
|
+
}
|
|
94
|
+
const projectRoot = await assertProject(args.project).catch(e => { throw configError(e.message); });
|
|
95
|
+
await loadConfig(projectRoot, args.alias);
|
|
96
|
+
const weekStart = args.week || ymd(currentIsoMonday());
|
|
97
|
+
const { fromIso, toIso } = weekBounds(weekStart);
|
|
98
|
+
|
|
99
|
+
const client = await buildClient({ fixture: args.fixture });
|
|
100
|
+
const startedAt = new Date().toISOString();
|
|
101
|
+
|
|
102
|
+
let messages;
|
|
103
|
+
try {
|
|
104
|
+
messages = await client.listMessages(args.entity, fromIso, toIso);
|
|
105
|
+
} catch (e) {
|
|
106
|
+
const retryable = !e.status || [429, 502, 503, 504].includes(e.status);
|
|
107
|
+
await updateCell(projectRoot, args.alias, SOURCE, args.entity, weekStart, { last_status: retryable ? 'deferred' : 'failed', last_error: e.message });
|
|
108
|
+
if (retryable && !args.dryRun) await enqueue(projectRoot, args.alias, { source: SOURCE, entity: args.entity, weekStart, signature: 'fetch-failed', reason: e.message });
|
|
109
|
+
if (!retryable && !args.dryRun) await emitLearningCandidate({ projectRoot, alias: args.alias, source: SOURCE, entity: args.entity, week: weekStart, error: { signature: 'fetch-failed', message: e.message, status: e.status }, context: { runner: 'pull-teams' } });
|
|
110
|
+
emit({ source: SOURCE, entity: args.entity, week: weekStart, status: retryable ? 'deferred' : 'failed', errors: [{ message: e.message, status: e.status }] });
|
|
111
|
+
return retryable ? 1 : 0;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const chatHash = shortHash(args.entity);
|
|
115
|
+
|
|
116
|
+
if (messages.length === 0) {
|
|
117
|
+
await updateCell(projectRoot, args.alias, SOURCE, args.entity, weekStart, { last_status: 'no-activity', items_pulled: 0, chat_hash: chatHash });
|
|
118
|
+
emit({ source: SOURCE, entity: args.entity, week: weekStart, status: 'no-activity', items_pulled: 0, files_written: [] });
|
|
119
|
+
return 0;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const outDir = path.join(sourceDir(projectRoot, args.alias, SOURCE), chatHash, weekStart);
|
|
123
|
+
const filesWritten = [];
|
|
124
|
+
if (!args.dryRun) {
|
|
125
|
+
const r1 = await writeAtomic(path.join(outDir, 'messages.yml'), YAML.stringify(messages));
|
|
126
|
+
const r2 = await writeAtomic(path.join(outDir, 'chat.yml'), YAML.stringify({ chat_id: args.entity, hash: chatHash }));
|
|
127
|
+
const r3 = await writeAtomic(path.join(outDir, 'index.md'), renderIndexMd({ chatId: args.entity, messages, weekStart, pulledAt: startedAt }), { skipIfUnchanged: false });
|
|
128
|
+
for (const r of [r1, r2, r3]) if (r.written !== false) filesWritten.push(path.relative(projectRoot, r.path));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
await clear(projectRoot, args.alias, SOURCE, args.entity).catch(() => {});
|
|
132
|
+
|
|
133
|
+
await updateCell(projectRoot, args.alias, SOURCE, args.entity, weekStart, {
|
|
134
|
+
last_status: 'captured',
|
|
135
|
+
items_pulled: messages.length,
|
|
136
|
+
chat_hash: chatHash,
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
if (!args.dryRun) {
|
|
140
|
+
await appendRunLog(projectRoot, { runner: 'pull-teams', alias: args.alias, entity: args.entity, week: weekStart, status: 'captured', items_pulled: messages.length });
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
emit({
|
|
144
|
+
source: SOURCE, entity: args.entity, week: weekStart, status: 'captured',
|
|
145
|
+
items_pulled: messages.length, chat_hash: chatHash,
|
|
146
|
+
files_written: filesWritten, ledger_key: `teams::${args.entity}::${weekStart}`,
|
|
147
|
+
});
|
|
148
|
+
return 0;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function renderIndexMd({ chatId, messages, weekStart, pulledAt }) {
|
|
152
|
+
const lines = [
|
|
153
|
+
`# Teams chat — ${chatId} — week ${weekStart}`,
|
|
154
|
+
'',
|
|
155
|
+
`- chat_id: ${chatId}`,
|
|
156
|
+
`- week_start: ${weekStart}`,
|
|
157
|
+
`- messages: ${messages.length}`,
|
|
158
|
+
`- pulled_at: ${pulledAt}`,
|
|
159
|
+
'',
|
|
160
|
+
'## Messages',
|
|
161
|
+
];
|
|
162
|
+
for (const m of messages) {
|
|
163
|
+
const from = (m.from && m.from.user && m.from.user.displayName) || (m.from && m.from.application && m.from.application.displayName) || '?';
|
|
164
|
+
const preview = (m.body && m.body.content || '').replace(/<[^>]+>/g, '').replace(/\s+/g, ' ').slice(0, 120);
|
|
165
|
+
lines.push(`- [${m.createdDateTime}] **${from}**: ${preview}`);
|
|
166
|
+
}
|
|
167
|
+
lines.push('');
|
|
168
|
+
return lines.join('\n');
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
main().then(code => { process.exitCode = code; }).catch(e => {
|
|
172
|
+
emit({ source: SOURCE, status: 'failed', errors: [{ message: e.message, code: e.exitCode || 'unknown' }] });
|
|
173
|
+
process.exitCode = e.exitCode || 1;
|
|
174
|
+
});
|
|
@@ -228,12 +228,19 @@ async function main() {
|
|
|
228
228
|
// v5.9.0: post-pass — unified references pool. Scans Evidence for URLs and
|
|
229
229
|
// builds a project-shared dedup index with HTTP snapshots for external links.
|
|
230
230
|
let referencesResult = null;
|
|
231
|
+
let stateResult = null;
|
|
231
232
|
if (!args.dryRun) {
|
|
232
233
|
const refsRunner = path.join(HERE, 'pull-references.mjs');
|
|
233
234
|
const refsArgv = ['--project', args.project];
|
|
234
235
|
if (args.force) refsArgv.push('--refresh');
|
|
235
236
|
const r = await spawnRunner(refsRunner, refsArgv);
|
|
236
|
-
referencesResult = { source: 'references', exit_code: r.
|
|
237
|
+
referencesResult = { source: 'references', exit_code: r.exitCode, stdout: r.stdout?.slice(0, 4000), stderr: r.stderr?.slice(0, 1000) };
|
|
238
|
+
|
|
239
|
+
// v5.9.0 / v6.2.0: post-pass — deterministic State/ generator. Inventory
|
|
240
|
+
// only; build-state LLM skill remains the synthesis layer.
|
|
241
|
+
const stateRunner = path.join(HERE, 'pull-state.mjs');
|
|
242
|
+
const s = await spawnRunner(stateRunner, ['--project', args.project]);
|
|
243
|
+
stateResult = { source: 'state', exit_code: s.exitCode, stdout: s.stdout?.slice(0, 4000), stderr: s.stderr?.slice(0, 1000) };
|
|
237
244
|
}
|
|
238
245
|
|
|
239
246
|
const learning_candidates_total = args.dryRun ? 0 : await readCandidateCount(args.project);
|
|
@@ -298,6 +305,7 @@ async function main() {
|
|
|
298
305
|
results,
|
|
299
306
|
skipped_targets: skipped,
|
|
300
307
|
references: referencesResult,
|
|
308
|
+
state: stateResult,
|
|
301
309
|
learning_candidates_total,
|
|
302
310
|
});
|
|
303
311
|
return 0;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"folders": [
|
|
3
|
+
{ "id": "AAMkAGI=", "displayName": "23. ABN AMRO" }
|
|
4
|
+
],
|
|
5
|
+
"messagesByFolder": {
|
|
6
|
+
"AAMkAGI=": [
|
|
7
|
+
{ "id": "m1", "subject": "Kickoff agenda", "receivedDateTime": "2026-05-25T08:00:00Z", "from": { "emailAddress": { "address": "ushak@abnamro.com", "name": "Alice" } } },
|
|
8
|
+
{ "id": "m2", "subject": "Architecture review notes", "receivedDateTime": "2026-05-27T14:30:00Z", "from": { "emailAddress": { "address": "bob@abnamro.com", "name": "Bob" } } },
|
|
9
|
+
{ "id": "m3", "subject": "Out of window — prior week", "receivedDateTime": "2026-05-18T09:00:00Z", "from": { "emailAddress": { "address": "carol@abnamro.com", "name": "Carol" } } },
|
|
10
|
+
{ "id": "m4", "subject": "Out of window — next week", "receivedDateTime": "2026-06-01T09:00:00Z", "from": { "emailAddress": { "address": "dave@abnamro.com", "name": "Dave" } } }
|
|
11
|
+
]
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"meeting": {
|
|
3
|
+
"id": "MSoxMjM0NTY3OA==",
|
|
4
|
+
"subject": "ABN AMRO — Architecture Review",
|
|
5
|
+
"startDateTime": "2026-05-26T15:00:00Z",
|
|
6
|
+
"endDateTime": "2026-05-26T16:00:00Z",
|
|
7
|
+
"joinWebUrl": "https://teams.microsoft.com/meet/31827546911768?p=YOgZHuaffeF0MGBCNt"
|
|
8
|
+
},
|
|
9
|
+
"transcript": "WEBVTT\n\n00:00:00.000 --> 00:00:05.000\nAlice: Welcome everyone.\n\n00:00:05.000 --> 00:00:10.000\nBob: Thanks. Let's start with the agenda.\n\n00:00:10.000 --> 00:00:20.000\nCarol: Architecture diagram is ready for review.\n"
|
|
10
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"meeting": {
|
|
3
|
+
"id": "MSoxMjM0NTY3OA==",
|
|
4
|
+
"subject": "ABN AMRO — Discovery (transcript pending)",
|
|
5
|
+
"startDateTime": "2026-05-28T09:00:00Z",
|
|
6
|
+
"endDateTime": "2026-05-28T10:00:00Z",
|
|
7
|
+
"joinWebUrl": "https://teams.microsoft.com/meet/26457824867211?p=zdSjg6cqHoHPA8Hx11"
|
|
8
|
+
},
|
|
9
|
+
"transcript": null
|
|
10
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"section": {
|
|
3
|
+
"id": "015a0ac9-58f4-4f34-98b3-a60075b36627",
|
|
4
|
+
"displayName": "ABN AMRO"
|
|
5
|
+
},
|
|
6
|
+
"resolvedSection": {
|
|
7
|
+
"id": "015a0ac9-58f4-4f34-98b3-a60075b36627",
|
|
8
|
+
"displayName": "ABN AMRO"
|
|
9
|
+
},
|
|
10
|
+
"pages": [
|
|
11
|
+
{
|
|
12
|
+
"id": "p1",
|
|
13
|
+
"title": "Kickoff Notes",
|
|
14
|
+
"lastModifiedDateTime": "2026-05-25T10:00:00Z",
|
|
15
|
+
"content": "<html><body><h1>Kickoff</h1><p>Project goals discussed.</p></body></html>"
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"id": "p2",
|
|
19
|
+
"title": "Architecture Notes",
|
|
20
|
+
"lastModifiedDateTime": "2026-05-27T14:00:00Z",
|
|
21
|
+
"content": "<html><body><h1>Architecture</h1><p>Three-tier proposed.</p></body></html>"
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"id": "p3",
|
|
25
|
+
"title": "Stale page",
|
|
26
|
+
"lastModifiedDateTime": "2026-05-18T08:00:00Z",
|
|
27
|
+
"content": "<html><body>Out of window</body></html>"
|
|
28
|
+
}
|
|
29
|
+
]
|
|
30
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"section": {
|
|
3
|
+
"id": "015a0ac9-58f4-4f34-98b3-a60075b36627",
|
|
4
|
+
"displayName": "ABN AMRO"
|
|
5
|
+
},
|
|
6
|
+
"resolvedSection": null,
|
|
7
|
+
"pages": [
|
|
8
|
+
{
|
|
9
|
+
"id": "p1",
|
|
10
|
+
"title": "Page with content",
|
|
11
|
+
"lastModifiedDateTime": "2026-05-25T10:00:00Z",
|
|
12
|
+
"content": "<html><body>OK</body></html>"
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"id": "p2",
|
|
16
|
+
"title": "Page body unavailable",
|
|
17
|
+
"lastModifiedDateTime": "2026-05-26T11:00:00Z",
|
|
18
|
+
"content": null
|
|
19
|
+
}
|
|
20
|
+
]
|
|
21
|
+
}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
{
|
|
2
|
-
"
|
|
3
|
-
"
|
|
4
|
-
|
|
1
|
+
{
|
|
2
|
+
"folders": [{ "id": "F1", "displayName": "Project Inbox" }],
|
|
3
|
+
"messagesByFolder": {
|
|
4
|
+
"F1": [
|
|
5
|
+
{ "id": "m1", "subject": "Kickoff", "receivedDateTime": "2026-05-26T10:00:00Z", "from": { "emailAddress": { "address": "alice@example.com", "name": "Alice" } } }
|
|
6
|
+
]
|
|
7
|
+
}
|
|
5
8
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
{
|
|
2
|
-
"
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
{
|
|
2
|
+
"messagesByChat": {
|
|
3
|
+
"19:abc@thread.v2": [
|
|
4
|
+
{ "id": "1748260800000", "createdDateTime": "2026-05-26T10:00:00Z", "from": { "user": { "displayName": "Alice" } }, "body": { "content": "kickoff", "contentType": "text" } }
|
|
5
|
+
]
|
|
6
|
+
}
|
|
5
7
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"itemsBySite": {
|
|
3
|
+
"https://abnamro.sharepoint.com/sites/TeamAlfred": [
|
|
4
|
+
{ "id": "01ABC", "name": "Architecture.pptx", "webUrl": "https://abnamro.sharepoint.com/sites/TeamAlfred/Architecture.pptx", "lastModifiedDateTime": "2026-05-25T11:00:00Z" },
|
|
5
|
+
{ "id": "01DEF", "name": "DiscoveryNotes.docx", "webUrl": "https://abnamro.sharepoint.com/sites/TeamAlfred/DiscoveryNotes.docx", "lastModifiedDateTime": "2026-05-27T16:00:00Z" },
|
|
6
|
+
{ "id": "01GHI", "name": "Stale.xlsx", "webUrl": "https://abnamro.sharepoint.com/sites/TeamAlfred/Stale.xlsx", "lastModifiedDateTime": "2026-05-18T09:00:00Z" }
|
|
7
|
+
],
|
|
8
|
+
"https://contoso-evil.sharepoint.com/sites/X": [
|
|
9
|
+
{ "id": "01ZZZ", "name": "Should never be reached.docx", "webUrl": "https://contoso-evil.sharepoint.com/sites/X/Y.docx", "lastModifiedDateTime": "2026-05-26T10:00:00Z" }
|
|
10
|
+
]
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"messagesByChat": {
|
|
3
|
+
"19:e17bf64ba941442590dae0824cc4784c@thread.v2": [
|
|
4
|
+
{ "id": "1747000000000", "createdDateTime": "2026-05-25T09:15:00Z", "from": { "user": { "displayName": "Alice" } }, "body": { "content": "<p>Status update</p>" } },
|
|
5
|
+
{ "id": "1747100000000", "createdDateTime": "2026-05-26T13:00:00Z", "from": { "user": { "displayName": "Bob" } }, "body": { "content": "<p>Will review the doc</p>" } },
|
|
6
|
+
{ "id": "1747200000000", "createdDateTime": "2026-05-28T08:30:00Z", "from": { "user": { "displayName": "Carol" } }, "body": { "content": "<p>Action items captured</p>" } },
|
|
7
|
+
{ "id": "1746000000000", "createdDateTime": "2026-05-18T10:00:00Z", "from": { "user": { "displayName": "Stale" } }, "body": { "content": "<p>Out of window</p>" } }
|
|
8
|
+
],
|
|
9
|
+
"19:empty@thread.v2": []
|
|
10
|
+
}
|
|
11
|
+
}
|