session-orchestrator 3.17.0 → 3.20.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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor/rules/030-wave-execution.mdc +17 -1
- package/CHANGELOG.md +185 -412
- package/README.md +12 -9
- package/SECURITY.md +190 -27
- package/agents/AGENTS.md +20 -3
- package/agents/code-implementer.md +6 -6
- package/agents/db-specialist.md +1 -1
- package/agents/qa-strategist.md +31 -6
- package/agents/schemas/qa-strategist.schema.json +27 -0
- package/agents/schemas/test-writer.schema.json +60 -2
- package/agents/security-reviewer.md +1 -1
- package/agents/session-reviewer.md +1 -1
- package/agents/test-writer.md +29 -10
- package/agents/ui-developer.md +1 -1
- package/commands/contract-version-bump.md +28 -0
- package/commands/portfolio.md +1 -1
- package/commands/session.md +6 -2
- package/docs/USER-GUIDE.md +9 -4
- package/docs/ci-setup.md +121 -7
- package/docs/codex-setup.md +1 -1
- package/docs/components.md +6 -6
- package/docs/cursor-setup.md +22 -9
- package/docs/events-schema.md +5 -1
- package/docs/instruction-delivery.md +794 -0
- package/docs/rule-authoring.md +58 -9
- package/docs/session-config-reference.md +245 -50
- package/docs/session-config-template.md +39 -26
- package/hooks/_lib/guard-source-loader.mjs +680 -0
- package/hooks/_lib/lock-bootstrap.mjs +21 -0
- package/hooks/_lib/vcs-create-matcher.mjs +119 -0
- package/hooks/config-protection.mjs +0 -0
- package/hooks/enforce-commands.mjs +226 -19
- package/hooks/enforce-scope.mjs +133 -9
- package/hooks/hooks-codex.json +1 -1
- package/hooks/hooks-cursor.json +11 -2
- package/hooks/hooks-pi.json +10 -0
- package/hooks/hooks.json +21 -1
- package/hooks/on-session-end.mjs +178 -18
- package/hooks/on-session-start.mjs +30 -4
- package/hooks/post-bash-write-verify.mjs +977 -0
- package/hooks/post-subagent-discovery-validator.mjs +256 -41
- package/hooks/pre-bash-destructive-guard.mjs +616 -164
- package/hooks/pre-bash-issue-budget.mjs +167 -0
- package/hooks/pre-bash-sessions-ledger-guard.mjs +1054 -0
- package/hooks/pre-bash-templates-first.mjs +96 -63
- package/hooks/subagent-telemetry.mjs +527 -37
- package/package.json +6 -3
- package/pi/prompts/contract-version-bump.md +12 -0
- package/rules/README.md +32 -0
- package/scripts/archive-closed-prds.mjs +12 -22
- package/scripts/autopilot-multi.mjs +103 -20
- package/scripts/backfill-abandoned-sessions.mjs +160 -4
- package/scripts/backfill-learnings-from-vault.mjs +967 -0
- package/scripts/check-doc-consistency.sh +17 -1
- package/scripts/emit-session.mjs +3 -40
- package/scripts/eval-session.mjs +50 -9
- package/scripts/fleet-instruction-scan.mjs +141 -0
- package/scripts/lib/autopilot/mr-draft.mjs +31 -1
- package/scripts/lib/autopilot/worktree-pipeline.mjs +113 -5
- package/scripts/lib/backlog-scan.mjs +39 -6
- package/scripts/lib/blocked-commands-policy.mjs +340 -0
- package/scripts/lib/ci-status-banner.mjs +75 -12
- package/scripts/lib/claude-md-budget-lint.mjs +283 -34
- package/scripts/lib/command-blocker.mjs +1273 -58
- package/scripts/lib/config/config-protection.mjs +2 -1
- package/scripts/lib/config/drift-check.mjs +9 -1
- package/scripts/lib/config/gitlab-portfolio.mjs +1 -1
- package/scripts/lib/config/issue-budget.mjs +123 -0
- package/scripts/lib/config/reconcile.mjs +21 -0
- package/scripts/lib/config/section-extractor.mjs +121 -1
- package/scripts/lib/config-schema.mjs +23 -3
- package/scripts/lib/config.mjs +17 -0
- package/scripts/lib/convergence-monitor.mjs +49 -3
- package/scripts/lib/description-surface.mjs +535 -0
- package/scripts/lib/dispatcher/enumerate.mjs +26 -40
- package/scripts/lib/ecosystem-wizard/config-writer.mjs +26 -24
- package/scripts/lib/ecosystem-wizard/wizard-prompt.mjs +1 -1
- package/scripts/lib/eval/engine.mjs +47 -5
- package/scripts/lib/events.mjs +59 -7
- package/scripts/lib/gates/gate-full.mjs +15 -3
- package/scripts/lib/gates/gate-helpers.mjs +132 -6
- package/scripts/lib/gitlab-ops/stale-mr-sweep.mjs +28 -8
- package/scripts/lib/gitlab-portfolio/aggregator.mjs +8 -2
- package/scripts/lib/gitlab-portfolio/cli.mjs +1 -1
- package/scripts/lib/handover-gate.mjs +7 -3
- package/scripts/lib/hardening.mjs +9 -9
- package/scripts/lib/harness-audit/categories/category4.mjs +9 -3
- package/scripts/lib/instruction-budget-guard.mjs +402 -51
- package/scripts/lib/io.mjs +345 -10
- package/scripts/lib/issue-budget.mjs +269 -0
- package/scripts/lib/issue-close-strip-labels.mjs +39 -9
- package/scripts/lib/label-scope.mjs +47 -0
- package/scripts/lib/learnings/affinity.mjs +434 -0
- package/scripts/lib/learnings/candidates.mjs +736 -0
- package/scripts/lib/learnings/expiry-sweep.mjs +408 -53
- package/scripts/lib/learnings/judgment.mjs +782 -0
- package/scripts/lib/learnings/kebab.mjs +128 -0
- package/scripts/lib/learnings/schema.mjs +43 -3
- package/scripts/lib/learnings/select.mjs +550 -0
- package/scripts/lib/lock-reaper.mjs +1 -2
- package/scripts/lib/memory-proposals/schema.mjs +36 -1
- package/scripts/lib/peer-discovery.mjs +645 -0
- package/scripts/lib/pi-hook-bridge.mjs +146 -17
- package/scripts/lib/product-repo-detect.mjs +9 -8
- package/scripts/lib/project-hygiene.mjs +432 -0
- package/scripts/lib/quality-gate.mjs +167 -0
- package/scripts/lib/recommendations-v0.mjs +1 -1
- package/scripts/lib/reconcile/eligibility.mjs +1 -1
- package/scripts/lib/reconcile/emitter.mjs +128 -24
- package/scripts/lib/reconcile/engine.mjs +156 -54
- package/scripts/lib/reconcile/idempotency.mjs +114 -14
- package/scripts/lib/reconcile/renderer.mjs +141 -25
- package/scripts/lib/reconcile/sanitize.mjs +518 -0
- package/scripts/lib/reconcile/writer.mjs +95 -1
- package/scripts/lib/reconcile-nudge-banner.mjs +65 -9
- package/scripts/lib/resource-probe/evaluate.mjs +70 -4
- package/scripts/lib/resource-probe.mjs +19 -0
- package/scripts/lib/rule-loader.mjs +6 -0
- package/scripts/lib/scope-baseline.mjs +564 -0
- package/scripts/lib/scope-gate.mjs +568 -145
- package/scripts/lib/session-close-backfill.mjs +63 -8
- package/scripts/lib/session-end/phase-skip.mjs +1 -0
- package/scripts/lib/session-id.mjs +221 -41
- package/scripts/lib/session-lock.mjs +304 -6
- package/scripts/lib/session-record-repair.mjs +551 -0
- package/scripts/lib/session-schema/constants.mjs +22 -3
- package/scripts/lib/session-schema/serializer.mjs +54 -0
- package/scripts/lib/session-schema/validator.mjs +16 -0
- package/scripts/lib/session-schema.mjs +1 -0
- package/scripts/lib/session-token-rollup.mjs +68 -6
- package/scripts/lib/sessions-integrity-banner.mjs +294 -0
- package/scripts/lib/sessions-staleness-banner.mjs +121 -12
- package/scripts/lib/skill-evolution/idempotency.mjs +135 -16
- package/scripts/lib/skill-evolution/mr-opener.mjs +9 -1
- package/scripts/lib/soul-resolve.mjs +12 -0
- package/scripts/lib/spiral-carryover.mjs +142 -30
- package/scripts/lib/state-md/mission-status.mjs +53 -3
- package/scripts/lib/subagents-schema.mjs +43 -9
- package/scripts/lib/test-runner/issue-reconcile.mjs +53 -13
- package/scripts/lib/tests-src-ratio.mjs +484 -0
- package/scripts/lib/tmux-layout/telemetry.mjs +43 -10
- package/scripts/lib/validate/check-agents.mjs +56 -0
- package/scripts/lib/validate/check-banner-parity.mjs +376 -0
- package/scripts/lib/validate/check-guard-requires-parity.mjs +1148 -0
- package/scripts/lib/validate/check-hooks-symmetry.mjs +244 -10
- package/scripts/lib/validate/check-learning-provenance.mjs +511 -0
- package/scripts/lib/validate/check-owner-leakage.mjs +3 -3
- package/scripts/lib/validate/check-rules.mjs +244 -36
- package/scripts/lib/validate/check-test-value-bans.mjs +782 -0
- package/scripts/lib/validate/check-unicode-safety.mjs +1 -0
- package/scripts/lib/validate/check-unwired-features.mjs +549 -0
- package/scripts/lib/validate-vendored-rules.mjs +10 -2
- package/scripts/lib/vault-archive.mjs +17 -2
- package/scripts/lib/vault-backfill/glab.mjs +8 -0
- package/scripts/lib/vault-mirror/process.mjs +30 -0
- package/scripts/lib/vault-mirror/render-sessions.mjs +293 -36
- package/scripts/lib/vcs-repo-spec.mjs +362 -0
- package/scripts/lib/wave-resource-gate.mjs +115 -11
- package/scripts/lib/worktree/listing.mjs +44 -7
- package/scripts/mcp-server.sh +17 -3
- package/scripts/measure-context-overhead.sh +151 -0
- package/scripts/memory-propose.mjs +72 -9
- package/scripts/print-applicable-rules.mjs +218 -16
- package/scripts/print-learnings-index.mjs +474 -0
- package/scripts/release.mjs +534 -0
- package/scripts/repair-invalid-sessions.mjs +209 -0
- package/scripts/run-quality-gate.mjs +123 -5
- package/scripts/sweep-expired-learnings.mjs +192 -32
- package/scripts/validate-plugin.mjs +21 -0
- package/scripts/validate-wave-scope.mjs +182 -17
- package/scripts/vault-integration-watcher.mjs +32 -10
- package/skills/_shared/config-reading.md +2 -2
- package/skills/bootstrap/fast-template.md +1 -1
- package/skills/brainstorm/soul.md +47 -1
- package/skills/claude-md-drift-check/checker.mjs +145 -28
- package/skills/contract-version-bump/SKILL.md +219 -0
- package/skills/discovery/SKILL.md +4 -4
- package/skills/discovery/issue-templates.md +11 -11
- package/skills/discovery/probes-audit.md +1 -1
- package/skills/discovery/probes-feature.md +1 -1
- package/skills/discovery/probes-session.md +26 -5
- package/skills/ecosystem-health/SKILL.md +1 -1
- package/skills/ecosystem-health/wizard.md +4 -4
- package/skills/evolve/SKILL.md +117 -18
- package/skills/gitlab-ops/SKILL.md +25 -12
- package/skills/gitlab-portfolio/SKILL.md +2 -2
- package/skills/grill/soul.md +44 -1
- package/skills/hook-development/SKILL.md +1 -1
- package/skills/mode-selector/SKILL.md +1 -1
- package/skills/npm-publish/SKILL.md +17 -1
- package/skills/plan/SKILL.md +5 -5
- package/skills/plan/mode-feature.md +4 -4
- package/skills/plan/mode-new.md +10 -10
- package/skills/plan/mode-retro.md +1 -1
- package/skills/plan/soul.md +46 -3
- package/skills/quality-gates/SKILL.md +1 -1
- package/skills/reconcile/SKILL.md +21 -4
- package/skills/session-end/SKILL.md +34 -36
- package/skills/session-end/discovery-scan.md +4 -2
- package/skills/session-end/drift-operations.md +4 -4
- package/skills/session-end/metrics-collection.md +13 -0
- package/skills/session-end/phase-3-2-docs-verification.md +1 -1
- package/skills/session-end/phase-3-6-tail.md +32 -2
- package/skills/session-end/plan-verification.md +6 -7
- package/skills/session-end/session-metrics-write.md +2 -0
- package/skills/session-end/vault-operations.md +1 -1
- package/skills/session-end/verification-checklist.md +1 -1
- package/skills/session-plan/SKILL.md +6 -2
- package/skills/session-plan/wave-template.md +2 -0
- package/skills/session-start/SKILL.md +75 -7
- package/skills/session-start/phase-4-5-resource-health.md +15 -2
- package/skills/session-start/soul.md +41 -1
- package/skills/test-runner/SKILL.md +2 -2
- package/skills/vault-sync/validator.mjs +108 -7
- package/skills/wave-executor/SKILL.md +6 -7
- package/skills/wave-executor/circuit-breaker.md +2 -0
- package/skills/wave-executor/wave-loop.md +198 -80
- package/templates/_shared/loop.md +4 -4
|
@@ -23,24 +23,43 @@
|
|
|
23
23
|
* `confidence`, `expires-at`. Glob values are always double-quoted (the loader
|
|
24
24
|
* strips surrounding quotes), which keeps `*`/`[`/`{` safe.
|
|
25
25
|
*
|
|
26
|
+
* ── Untrusted-input containment (issue #1015) ────────────────────────────────
|
|
27
|
+
* Every field this renderer interpolates is AGENT-AUTHORED, and the file it
|
|
28
|
+
* produces becomes a project instruction delivered to every agent in every
|
|
29
|
+
* session — permanently, with no revocation. {@link renderRule} therefore
|
|
30
|
+
* applies `sanitize.mjs` at the render point, in two modes:
|
|
31
|
+
* - MACHINE values (`description`, `globs[]`, `host-class`, `learning-key`,
|
|
32
|
+
* `confidence`, `expires-at`, `id`, `source_session`) are ASSERTED and the
|
|
33
|
+
* record REJECTED (throw) on any violation — never silently repaired.
|
|
34
|
+
* - PROSE (`title`/`subject`, `insight`, `evidence`) is stripped of
|
|
35
|
+
* unambiguous non-content, hard-capped in bytes, and FRAMED in the
|
|
36
|
+
* `untrusted-content` envelope that states it is data, not instruction.
|
|
37
|
+
* The guard lives HERE and not only in the emitter for the same reason the
|
|
38
|
+
* brandmauer is re-checked below: `renderRule` is exported and callable with
|
|
39
|
+
* hand-built metadata, so the emitter's guards do not bind on it.
|
|
40
|
+
*
|
|
26
41
|
* @module reconcile/renderer
|
|
27
42
|
*/
|
|
28
43
|
|
|
29
44
|
import { createHash } from 'node:crypto';
|
|
30
45
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
46
|
+
import { kebab } from '../learnings/kebab.mjs';
|
|
47
|
+
import {
|
|
48
|
+
EVIDENCE_ITEM_MAX_BYTES,
|
|
49
|
+
EVIDENCE_MAX_BYTES,
|
|
50
|
+
EXPIRES_AT_RE,
|
|
51
|
+
HOST_CLASS_RE,
|
|
52
|
+
INSIGHT_MAX_BYTES,
|
|
53
|
+
LEARNING_KEY_RE,
|
|
54
|
+
PROVENANCE_TOKEN_RE,
|
|
55
|
+
TITLE_MAX_BYTES,
|
|
56
|
+
UNTRUSTED_BEGIN,
|
|
57
|
+
UNTRUSTED_END,
|
|
58
|
+
assertMachineToken,
|
|
59
|
+
assertSafeDescription,
|
|
60
|
+
assertSafeGlob,
|
|
61
|
+
sanitizeProse,
|
|
62
|
+
} from './sanitize.mjs';
|
|
44
63
|
|
|
45
64
|
/**
|
|
46
65
|
* Short, stable SHA-1 prefix (first 7 hex chars) of an input string.
|
|
@@ -93,20 +112,47 @@ export function deriveSlug(learning) {
|
|
|
93
112
|
* Array → one `- ` bullet per item; string → as-is; absent/empty →
|
|
94
113
|
* `(no evidence recorded)`.
|
|
95
114
|
*
|
|
115
|
+
* Untrusted (#1015): every item passes through {@link sanitizeProse} with a
|
|
116
|
+
* per-bullet byte cap, and the whole region is bounded by
|
|
117
|
+
* {@link EVIDENCE_MAX_BYTES}. Items that would cross the region budget are
|
|
118
|
+
* dropped with a machine-emitted note — visibly, never silently. Because
|
|
119
|
+
* `EVIDENCE_ITEM_MAX_BYTES < EVIDENCE_MAX_BYTES` by construction, at least the
|
|
120
|
+
* first item always fits.
|
|
121
|
+
*
|
|
96
122
|
* @param {unknown} evidence
|
|
97
123
|
* @returns {string}
|
|
124
|
+
* @throws {Error} when an item carries a delivery-wrapper forgery literal
|
|
98
125
|
*/
|
|
99
126
|
function renderEvidence(evidence) {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
127
|
+
const isArray = Array.isArray(evidence);
|
|
128
|
+
const items = isArray
|
|
129
|
+
? evidence.filter((e) => e !== null && e !== undefined && String(e) !== '')
|
|
130
|
+
: typeof evidence === 'string' && evidence !== ''
|
|
131
|
+
? [evidence]
|
|
132
|
+
: [];
|
|
133
|
+
|
|
134
|
+
if (items.length === 0) return '(no evidence recorded)';
|
|
135
|
+
|
|
136
|
+
/** @type {string[]} */
|
|
137
|
+
const lines = [];
|
|
138
|
+
let usedBytes = 0;
|
|
139
|
+
for (const item of items) {
|
|
140
|
+
const safe = sanitizeProse(item, { field: 'evidence', maxBytes: EVIDENCE_ITEM_MAX_BYTES });
|
|
141
|
+
const line = isArray ? `- ${safe}` : safe;
|
|
142
|
+
const width = Buffer.byteLength(line, 'utf8');
|
|
143
|
+
if (lines.length > 0 && usedBytes + width > EVIDENCE_MAX_BYTES) break;
|
|
144
|
+
lines.push(line);
|
|
145
|
+
usedBytes += width;
|
|
105
146
|
}
|
|
106
|
-
|
|
107
|
-
|
|
147
|
+
|
|
148
|
+
const omitted = items.length - lines.length;
|
|
149
|
+
if (omitted > 0) {
|
|
150
|
+
lines.push(
|
|
151
|
+
`- […${omitted} further evidence item(s) omitted by the reconciliation engine: ${EVIDENCE_MAX_BYTES}-byte region cap]`,
|
|
152
|
+
);
|
|
108
153
|
}
|
|
109
|
-
|
|
154
|
+
|
|
155
|
+
return lines.join('\n');
|
|
110
156
|
}
|
|
111
157
|
|
|
112
158
|
/**
|
|
@@ -120,6 +166,14 @@ function renderEvidence(evidence) {
|
|
|
120
166
|
* @returns {{ slug: string, path: string, content: string }}
|
|
121
167
|
* @throws {Error} when `metadata.globs` is empty AND `metadata.hostClass` is
|
|
122
168
|
* falsy — the never-always-on invariant (defends the emitter's guard).
|
|
169
|
+
* @throws {Error} (`reconcile-sanitize: rejecting record — …`) when a machine
|
|
170
|
+
* value is malformed (a `description` with a control char, a dangerous
|
|
171
|
+
* invisible, a wrapper-forgery literal or over its byte budget; a `globs[]`
|
|
172
|
+
* element with a quote/control char/invisible; a non-token
|
|
173
|
+
* `host-class`/`learning-key`/`id`/`source_session`; a non-finite
|
|
174
|
+
* `confidence`; a non-`YYYY-MM-DD` `expires-at`) or when prose forges the
|
|
175
|
+
* delivery wrapper's framing — issue #1015. The record is REJECTED, never
|
|
176
|
+
* repaired.
|
|
123
177
|
*/
|
|
124
178
|
export function renderRule(learning, metadata) {
|
|
125
179
|
if (metadata === null || typeof metadata !== 'object' || Array.isArray(metadata)) {
|
|
@@ -139,18 +193,59 @@ export function renderRule(learning, metadata) {
|
|
|
139
193
|
);
|
|
140
194
|
}
|
|
141
195
|
|
|
196
|
+
// ── Machine-value gate (#1015) — assert, never repair ─────────────────────
|
|
197
|
+
// Each of these is serialised into a frontmatter scalar (or, for the last
|
|
198
|
+
// two, into an inline-code span). A violation REJECTS the whole record: the
|
|
199
|
+
// engine catches the throw and records an audited `emit/render error: …`
|
|
200
|
+
// rejection, so a hostile record costs one rejected proposal, never a rule
|
|
201
|
+
// file. Repairing instead of rejecting would invent information — see
|
|
202
|
+
// `sanitize.mjs` for the per-field reasoning.
|
|
203
|
+
// `description` is the ONE agent-authored value emitted outside the untrusted
|
|
204
|
+
// envelope (a frontmatter scalar cannot carry the envelope's HTML comment —
|
|
205
|
+
// the loader would read the comment AS the description). It therefore gets
|
|
206
|
+
// equivalent neutralisation instead of framing: no frontmatter escape, no
|
|
207
|
+
// smuggled invisibles, no delivery-wrapper forgery, bounded length. See
|
|
208
|
+
// `sanitize.mjs` § assertSafeDescription.
|
|
209
|
+
assertSafeDescription(metadata.description);
|
|
210
|
+
for (const glob of globs) assertSafeGlob(glob);
|
|
211
|
+
if (hostClass !== undefined) {
|
|
212
|
+
assertMachineToken(hostClass, { field: 'host-class', pattern: HOST_CLASS_RE });
|
|
213
|
+
}
|
|
214
|
+
assertMachineToken(metadata.learningKey, { field: 'learning-key', pattern: LEARNING_KEY_RE });
|
|
215
|
+
if (!Number.isFinite(metadata.confidence)) {
|
|
216
|
+
// A non-finite confidence is SILENTLY DROPPED by the loader's `Number(...)`
|
|
217
|
+
// coercion, so the rule would load with no confidence at all rather than
|
|
218
|
+
// fail loudly.
|
|
219
|
+
throw new Error(
|
|
220
|
+
`reconcile-sanitize: rejecting record — confidence must be a finite number (got ${JSON.stringify(metadata.confidence)})`,
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
assertMachineToken(metadata.expiresAt, { field: 'expires-at', pattern: EXPIRES_AT_RE });
|
|
224
|
+
|
|
142
225
|
const slug = deriveSlug(learning);
|
|
143
226
|
const path = `.claude/rules/${slug}.md`;
|
|
144
227
|
|
|
145
|
-
|
|
228
|
+
// Prose sources. `deriveSlug` is safe by construction (its `kebab` collapses
|
|
229
|
+
// every non-`[a-z0-9]` run, so no title can steer the output path), but the
|
|
230
|
+
// rendered H1 carries the raw value — `title` outranks `subject` and has no
|
|
231
|
+
// schema constraint at all, so it is capped and framed like any other prose.
|
|
232
|
+
const rawTitle =
|
|
146
233
|
(typeof learning.title === 'string' && learning.title !== '' ? learning.title : '') ||
|
|
147
234
|
(typeof learning.subject === 'string' && learning.subject !== '' ? learning.subject : '') ||
|
|
148
235
|
metadata.learningKey ||
|
|
149
236
|
slug;
|
|
237
|
+
const safeTitle = sanitizeProse(rawTitle, { field: 'title', maxBytes: TITLE_MAX_BYTES })
|
|
238
|
+
// The H1 is a single line: a newline in the title would end the heading and
|
|
239
|
+
// let the remainder render as top-level markdown of the attacker's choosing.
|
|
240
|
+
.replace(/[\t\n]+/g, ' ')
|
|
241
|
+
.trim();
|
|
242
|
+
// A title made only of stripped invisibles sanitises to '' — fall back to the
|
|
243
|
+
// slug so the H1 is never empty.
|
|
244
|
+
const humanTitle = safeTitle !== '' ? safeTitle : slug;
|
|
150
245
|
|
|
151
246
|
const insight =
|
|
152
247
|
typeof learning.insight === 'string' && learning.insight !== ''
|
|
153
|
-
? learning.insight
|
|
248
|
+
? sanitizeProse(learning.insight, { field: 'insight', maxBytes: INSIGHT_MAX_BYTES })
|
|
154
249
|
: '(no insight recorded)';
|
|
155
250
|
|
|
156
251
|
// ── Frontmatter (fixed key order; loader-parseable) ──────────────────────
|
|
@@ -182,11 +277,30 @@ export function renderRule(learning, metadata) {
|
|
|
182
277
|
fm.push('---');
|
|
183
278
|
|
|
184
279
|
// ── Body (free markdown; does not affect frontmatter parsing) ────────────
|
|
185
|
-
|
|
186
|
-
|
|
280
|
+
// Both provenance values are rendered INSIDE an inline-code span below, which
|
|
281
|
+
// an interior backtick would close — so they are asserted as machine tokens
|
|
282
|
+
// rather than escaped. The 'n/a' fallbacks are machine-emitted, not asserted.
|
|
283
|
+
const learningId = learning.id
|
|
284
|
+
? assertMachineToken(String(learning.id), {
|
|
285
|
+
field: 'learning-id',
|
|
286
|
+
pattern: PROVENANCE_TOKEN_RE,
|
|
287
|
+
})
|
|
288
|
+
: 'n/a';
|
|
289
|
+
const sourceSession = learning.source_session
|
|
290
|
+
? assertMachineToken(String(learning.source_session), {
|
|
291
|
+
field: 'source-session',
|
|
292
|
+
pattern: PROVENANCE_TOKEN_RE,
|
|
293
|
+
})
|
|
294
|
+
: 'n/a';
|
|
187
295
|
|
|
296
|
+
// The untrusted region (H1 + insight + Evidence) is FRAMED: the envelope
|
|
297
|
+
// states, in machine-generated text the record cannot forge, that everything
|
|
298
|
+
// inside is agent-authored data rather than an instruction to the reading
|
|
299
|
+
// agent. Provenance stays OUTSIDE the envelope — it is machine-derived and
|
|
300
|
+
// asserted above.
|
|
188
301
|
const body = [
|
|
189
302
|
'',
|
|
303
|
+
UNTRUSTED_BEGIN,
|
|
190
304
|
`# Auto-generated rule: ${humanTitle}`,
|
|
191
305
|
'',
|
|
192
306
|
insight,
|
|
@@ -194,6 +308,8 @@ export function renderRule(learning, metadata) {
|
|
|
194
308
|
'## Evidence',
|
|
195
309
|
renderEvidence(learning.evidence),
|
|
196
310
|
'',
|
|
311
|
+
UNTRUSTED_END,
|
|
312
|
+
'',
|
|
197
313
|
'<!-- provenance (auto-generated by the reconciliation engine — do not hand-edit) -->',
|
|
198
314
|
'## Provenance',
|
|
199
315
|
`- learning-key: \`${metadata.learningKey}\``,
|