feat(ms-ai-architect): C2.2 valgfritt fritekst-felt free-context.md i onboarding (TDD) [skip-docs]

Spor C / C2.2 (#3, akseptanse K3): fanger et fritt prosa-felt i onboarding og
overflater det (kappet) ambient i hver sesjon.

- lib/user-data.mjs: FREE_CONTEXT_FILE (utenfor ORG_FILES — valgfri, teller ikke
  mot fullføring), collapseProse() surfacer fri kontekst som ÉTT felt uansett
  markdown-struktur (ingen stille drop), capValue() ekstrahert (DRY).
- session-start-context.mjs: leser free-context.md valgfritt (ingen count++).
- onboarding-agent.md: ny Phase 6 (valgfri); onboard.md: prosess/Task/status.
- 11 agenter: uniform 6. bullet (grep 11/11).
- Tester FØR kode: user-data 25/25 (+9), test-hooks 11/11 (+1 K3-ambient).

Gates: validate 239/0/0 · kb-update 200 · kb-eval 100 · kb-integrity 192/192
(220 baseline-warns) · discovery 13/13 · gitleaks 3 pre-eksisterende.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-06-22 14:16:00 +02:00
commit 8ff73b7fe3
18 changed files with 225 additions and 11 deletions

View file

@ -19,6 +19,7 @@ import {
USER_DATA_DIRNAME,
CONFIG_FILENAME,
ORG_FILES,
FREE_CONTEXT_FILE,
resolveUserDataDir,
resolveOrgDir,
resolveConfigPath,
@ -173,3 +174,92 @@ test('buildOrgSummary — default cap keeps the summary compact (<= 25 lines)',
const lines = s.split('\n').filter(Boolean);
assert.ok(lines.length <= 25, `default cap should keep <=25 lines, got ${lines.length}`);
});
// ===========================================================================
// (C) Free-prose context (C2.2) — the optional sixth file (#3, acceptance K3)
// ===========================================================================
test('FREE_CONTEXT_FILE — optional free-prose filename, NOT part of ORG_FILES', () => {
assert.equal(FREE_CONTEXT_FILE, 'free-context.md');
assert.ok(
!ORG_FILES.includes(FREE_CONTEXT_FILE),
'free-context is optional and must not count toward onboarding completeness',
);
});
const FREE = [
'---',
'category: free-context',
'completed: true',
'last_updated: 2026-06-22',
'---',
'',
'# Fri kontekst',
'',
'## Fri kontekst',
'Vi migrerer fra on-prem til Azure i 2026.',
'',
].join('\n');
test('buildOrgSummary — surfaces free-context as a "Fri kontekst" field', () => {
const s = buildOrgSummary({ 'free-context.md': FREE });
assert.match(s, /Fri kontekst: Vi migrerer fra on-prem til Azure i 2026\./);
});
test('buildOrgSummary — free-context is robust to raw prose without any H2 (K3: no silent drop)', () => {
// The whole point of #3 is a single free field; it must surface even when the
// agent writes a plain paragraph with no markdown structure at all.
const s = buildOrgSummary({
'free-context.md': 'Vi har hatt en hendelse med Copilot som lekket interne dokumenter.',
});
assert.match(s, /Fri kontekst: Vi har hatt en hendelse med Copilot/);
});
test('buildOrgSummary — free-context strips frontmatter and all header lines', () => {
const s = buildOrgSummary({
'free-context.md': '---\ncategory: free-context\n---\n# Fri kontekst\n## Fri kontekst\nKun prosaen skal vises.\n',
});
assert.doesNotMatch(s, /category:/, 'frontmatter stripped');
assert.equal(s, 'Fri kontekst: Kun prosaen skal vises.');
});
test('buildOrgSummary — free-context appears AFTER the structured fields', () => {
const out = buildOrgSummary({
'free-context.md': '## Fri kontekst\nTilleggsnotat.',
'organization-profile.md': PROFILE,
});
assert.ok(
out.indexOf('Sektortype') < out.indexOf('Fri kontekst'),
'structured context leads; free prose trails',
);
});
test('buildOrgSummary — long free-context is capped with an ellipsis (budget guard)', () => {
const s = buildOrgSummary({ 'free-context.md': 'y'.repeat(500) }, { maxValueLen: 80 });
const line = s.split('\n').find((l) => l.startsWith('Fri kontekst:'));
assert.ok(line, 'free-context line present');
assert.match(line, /…$/, 'truncation marker appended');
assert.ok(line.length <= 'Fri kontekst: '.length + 80, `line too long: ${line.length}`);
});
test('buildOrgSummary — empty/whitespace/header-only free-context yields no field', () => {
assert.equal(buildOrgSummary({ 'free-context.md': ' \n\n' }), '');
assert.equal(
buildOrgSummary({ 'free-context.md': '---\ncategory: free-context\n---\n# Fri kontekst\n' }),
'',
'frontmatter + bare H1 (no prose) => nothing',
);
});
test('buildOrgSummary — free-context absent leaves the structured summary unchanged (back-compat)', () => {
const s = buildOrgSummary({ 'organization-profile.md': PROFILE });
assert.doesNotMatch(s, /Fri kontekst/);
});
test('buildOrgSummary — realistic full onboarding keeps free-context visible (K3)', () => {
const all = {};
for (const f of ORG_FILES) all[f] = `## ${f}-felt\nverdi for ${f}`;
all['free-context.md'] = '## Fri kontekst\nViktig tilleggskontekst som må være synlig.';
const s = buildOrgSummary(all); // default cap 25
assert.match(s, /Fri kontekst: Viktig tilleggskontekst/, 'free-context visible within default budget');
});

View file

@ -58,6 +58,20 @@ Statlig
Testetaten
ORGEOF
# C2.2: optional free-prose file — read for the ambient summary, NOT counted
# toward onboarding completeness. Proves K3 (free-text visible in interaction).
cat > "$TMPHOME/.claude/ms-ai-architect/org/free-context.md" <<'FREEEOF'
---
category: free-context
completed: true
---
# Fri kontekst
## Fri kontekst
Vi migrerer fra on-prem til Azure i 2026.
FREEEOF
OUTPUT=$(CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT" HOME="$TMPHOME" node "$SCRIPTS_DIR/session-start-context.mjs" 2>/dev/null)
if echo "$OUTPUT" | grep -q "Virksomhetskontekst (auto-injisert"; then
@ -72,6 +86,12 @@ else
fail "Ambient block missing org fact"
fi
if echo "$OUTPUT" | grep -q "Fri kontekst: Vi migrerer fra on-prem til Azure"; then
pass "Ambient block surfaces optional free-context (K3)"
else
fail "Ambient block missing free-context content (K3)"
fi
# Empty home (no user org, no plugin org) => onboarding nudge, NO ambient block.
TMPHOME2=$(mktemp -d)
OUTPUT=$(CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT" HOME="$TMPHOME2" node "$SCRIPTS_DIR/session-start-context.mjs" 2>/dev/null)