Harness Pulse
Single-page time-windowed product pulse. Phase 3 ships the first-run interview only: it converts vague intent into a concrete pulse: block in harness.config.json, refuses read-write DB credentials, and seeds from STRATEGY.md when present. The actual harness pulse run (Phases 2-4 of the runtime) is deferred to spec Phase 4.
When to Use
- Manually, when a project wants to start receiving daily pulse reports
- When
harness.config.json has no pulse: block and the user invokes /harness:pulse
- NOT for ad-hoc one-off metric queries (that's the analytics tool's job)
- NOT for replacing dashboards (pulse is a single-page summary, not a metrics platform)
- NOT for projects that have not yet decided what their key metrics are (run
harness-strategy first to write STRATEGY.md; pulse seeds from it)
Process
Iron Law
No PII reaches harness.config.json and no read-write DB credential is accepted. Both are interview-time gates; both are hard refusals (no warnings, no overrides without an explicit user-typed escape).
Phase 0: ROUTE BY CONFIG STATE
- Read
harness.config.json.
- If
pulse.enabled is set (true OR false), skip directly to "Phase 2: RUN" — deferred to spec Phase 4. For now, surface "pulse already configured; the run path ships in Phase 4" and stop.
- Otherwise enter Phase 1.
Phase 1: FIRST-RUN INTERVIEW
Read references/interview.md for the SMART pushback rules and the READ-WRITE-DB rejection rule. Both are mandatory.
Seed from STRATEGY.md. Call seed_pulse_from_strategy({ path: process.cwd() }) on the harness MCP server. Capture { name, keyMetrics, warnings } — the server does the file read and bullet extraction internally. Fallback when the MCP server is unavailable: node -e "import('@harness-engineering/core').then(m => console.log(JSON.stringify(m.seedFromStrategy({}))))" — only works when @harness-engineering/core is resolvable from the project.
- Surface
warnings to the user verbatim.
- If
name is non-null, confirm it as the product name; otherwise prompt.
- For each
keyMetric, walk it through the SMART bar in step 4.
Pick the lookback default. Ask in plain text — present numbered options ["24h", "7d", "30d", "custom"] and wait (do NOT use emit_interaction/AskUserQuestion; the human won't see it — it renders only to the model and the client collapses the call to "Called harness"). Default 24h per spec.
Identify the primary engagement event (e.g. session_started). Apply the SMART bar. If the user can't name one, set null and add a pending entry. Record the event name in primaryEvent.
Identify the value-realization event (e.g. plan_completed). SMART bar applies. Record in valueEvent.
Identify completion events (zero or more). SMART bar per item. Record in completionEvents.
Quality scoring (optional). Ask whether the user wants quality sampling on a single dimension (e.g. "did the plan deliver value"). Default off. If enabled, set qualityScoring: true and record qualityDimension.
Wire data sources. Ask which providers are available:
analytics: posthog, amplitude, mixpanel, custom — or null
tracing: sentry, datadog, custom — or null
payments: stripe, custom — or null
db: opt-in only, with the READ-WRITE-DB rejection rule active
For each non-null choice, check getPulseAdapter(name). If absent, surface the "Phase 4 will ship the adapter" warning from references/interview.md.
Walk every STRATEGY.md key metric through SMART. Map to an event when wired; otherwise append to pendingMetrics (or excludedMetrics if explicitly skipped). Cite STRATEGY.md when seeding so the user understands provenance.
Confirm the assembled config. Show the user the proposed pulse: block; ask for confirmation.
Write the config. Call write_pulse_config({ path: process.cwd(), config }) on the harness MCP server. The config argument is the assembled PulseConfig JSON. The MCP tool validates against PulseConfigSchema and refuses to touch disk on schema failure; passing the config as a JSON parameter (not through a shell) means user-supplied prose never crosses the shell tokenizer — no quoting, backtick, or $VAR hazard. The writer preserves all other config keys and writes harness.config.json.bak on first call.
Fallback for environments without the MCP server (only works when @harness-engineering/core is resolvable from the project): echo '<json-blob>' | node -e "import('@harness-engineering/core').then(m => m.writePulseConfig(JSON.parse(require('fs').readFileSync(0, 'utf-8')), { configPath: 'harness.config.json' }))".
Offer to register the product-pulse maintenance task. Deferred: Phase 6 of the spec wires it. For now, surface "the daily 8am product-pulse task will be registered automatically once Phase 6 of the feedback-loops spec ships; you can also run pulse on demand with /harness:pulse [window] once Phase 4 ships."
Run harness validate to confirm the new pulse: block parses.
Phase 2: RUN — deferred to spec Phase 4
Stub: when pulse.enabled === true, this phase will dispatch analytics/tracing/payments queries in parallel, run the SanitizeFn for each provider's response, and stash sanitized results for Phase 3. NOT YET IMPLEMENTED. The skill exits early with a "deferred to Phase 4" message if it reaches this phase.
Phase 3: ASSEMBLE — deferred
Phase 4: SAVE — deferred
Question-File Mode (opt-in)
The default is the interactive first-run interview above. Question-file mode is an opt-in variant for async, team-reviewable pulse configuration — useful when the metric/event decisions need review from data owners before the pulse: block is written. The skill writes its questions to a durable file, the human fills them in over time, and the skill reads them back. Enter it only when the human asks; never automatically. The full convention — file location, the [Answer]: tag, the read-back ritual, context hygiene — lives in references/question-file-mode.md; this section states only how it maps onto the pulse interview.
- WRITE. Assemble the same questions Phase 1 would ask (lookback window; primary engagement event; value-realization event; completion events; quality-scoring opt-in; data-source wiring; and one question per
STRATEGY.md Key metric when seeded) into docs/pulse/interviews/pulse-questions.md — a pulse-scoped home next to the pulse config it produces (no per-change slug) (or .harness/sessions/<slug>/interviews/pulse-questions.md when a session slug is known), each with an empty [Answer]:. Never write a DB connection string into the file — collect any db credential interactively so the READ-WRITE-DB rejection rule runs on a value that is not persisted to a committable file. Report the path and stop.
- READ-BACK. On the next run, re-read the file from disk and treat each filled
[Answer]: as that question's answer. Apply the SMART bar to every proposed metric/event exactly as in interactive mode, flagging failures in the file under the question. Unanswered questions route to pendingMetrics/null per the normal skip handling.
- CONTRADICTION PASS. Run the cross-answer contradiction pass (see
references/interview.md and references/question-file-mode.md) over the filled answers — most importantly the metric ⇄ approach category: a Key metric whose data source was never wired (e.g. "p95 latency" with no tracing source) is a contradiction, not merely a pending metric. Surface it; never auto-resolve.
- PROCEED. Continue to the unchanged Phase 1.9 confirm →
write_pulse_config path. File mode changes only how answers are gathered — the PII gate, the READ-WRITE-DB rejection, schema validation, and .bak behavior are identical.
Harness Integration
harness validate — Run after write_pulse_config; the existing pulse-schema validator catches malformed blocks.
- Harness MCP tools consumed by this skill (canonical execution path — no project-local
@harness-engineering/core required):
seed_pulse_from_strategy({ path }) — defensive STRATEGY.md reader; returns { name, keyMetrics, warnings }.
write_pulse_config({ path, config }) — atomic config update with .bak. Validates against PulseConfigSchema.
@harness-engineering/core primitives the MCP tools wrap (only directly relevant when developing inside the monorepo or when the MCP server is unavailable):
writePulseConfig(config, { configPath }), seedFromStrategy({ cwd }), getPulseAdapter(name) / listPulseAdapters() (Phase 4 populates), PulseConfigSchema / PII_FIELD_DENYLIST.
- Boundary with
harness-strategy — Strategy writes STRATEGY.md; pulse reads it to seed. Pulse never writes to STRATEGY.md.
- Boundary with
harness-observability — Observability designs what to instrument; pulse is the read-side companion that surfaces what was instrumented.
- Decision 6 (read-only) — Pulse refuses read-write DB credentials. Documented in
references/interview.md.
- Decision 7 (PII contract) — Every provider source must have a registered
SanitizeFn adapter. Phase 3 ships the registry; Phase 4 ships the adapters.
Success Criteria
- On a project with no
pulse: block, the interview produces a valid pulse: block in harness.config.json with all non-pulse keys preserved.
- A
harness.config.json.bak is written before mutation.
- A read-write DB credential is refused; the interview either accepts a read-only credential or sets
sources.db.enabled: false.
- When
STRATEGY.md exists, name and Key metrics seed the interview; missing/malformed STRATEGY.md soft-fails with warnings.
harness validate passes after the interview completes.
Rationalizations to Reject
These are common rationalizations that sound reasonable but lead to incorrect results. When you catch yourself thinking any of these, stop and follow the documented process instead.
| Rationalization |
Why It Is Wrong |
| "The user pasted an admin DB connection string, but they clearly know their setup — I'll accept it." |
The READ-WRITE-DB rejection is non-negotiable: no flag, no override, no "I know what I'm doing" path. Refuse, cite Decision 6, and set sources.db.enabled: false. A pulse report never needs write access. |
| "'Engagement' is a bit vague, but I know what they mean — I'll record it without SMART pushback." |
SMART pushback is mandatory on every proposed metric/event. Silently accepting a vague name pollutes the config and produces an unmeasurable pulse. Push back until the metric names a concrete, measurable event. |
"It's just one field — I'll hand-edit the pulse: block into harness.config.json directly." |
write_pulse_config is the only sanctioned write path: it preserves all non-pulse keys, validates against PulseConfigSchema, and writes the .bak. A hand-edit bypasses the schema gate and can clobber sibling config. |
| "STRATEGY.md already lists the key metrics — I'll copy them into the config as-is." |
Every seeded key metric must still clear the SMART bar and be mapped to a wired event. Metrics with no data source belong in pendingMetrics, not the live block — copying them wholesale ships a config that silently can't be computed. |
"The config assembled cleanly — I'll skip harness validate." |
A malformed pulse: block silently breaks the daily product-pulse task once the run path ships. Validation is a gate that must pass before exit, not an optional final touch. |
Examples
Example: greenfield (no STRATEGY.md, no existing pulse block)
- Phase 0: route to Phase 1.
- Phase 1.1:
seedFromStrategy returns { name: null, keyMetrics: [], warnings: ['STRATEGY.md not found'] }.
- Phase 1.2-7: prompt user; collect
lookbackDefault: '24h', primaryEvent: 'session_started', valueEvent: 'plan_completed', sources.analytics: 'posthog' (with adapter-availability warning), sources.db.enabled: false.
- Phase 1.10:
write_pulse_config writes the block; .bak saved.
- Phase 1.12:
harness validate passes.
Example: STRATEGY.md present with 3 Key metrics
- Phase 1.1: seed returns
{ name: 'Acme', keyMetrics: ['DAU', 'plans/week', 'p95 latency'] }.
- Phase 1.8: walk each metric through SMART.
- "DAU" → mapped to
session_started count over 24h, accepted.
- "plans/week" → mapped to
plan_completed count over 7d, accepted (recorded as a future custom window).
- "p95 latency" → no tracing source wired yet; lands in
pendingMetrics.
Example: user offers an admin DB credential
- Phase 1.7: user pastes
postgresql://admin:pwd@host/db.
- Skill matches
admin username against the rejection list; refuses; cites Decision 6.
- User declines to provide a read-only credential; skill writes
sources.db.enabled: false.
Gates
- READ-WRITE-DB rejection is non-negotiable. No flag, no override, no "I know what I'm doing" path. Refuse and document.
- SMART pushback is mandatory on every proposed metric/event. Silently accepting a vague name pollutes the corpus.
write_pulse_config (or writePulseConfig when invoked directly) is the only sanctioned write path. Do not hand-edit harness.config.json. The writer is the layer that preserves non-pulse keys and writes the .bak.
harness validate must pass before exit. A malformed pulse: block silently breaks the daily task once Phase 4 ships.
Escalation
- User insists on read-write DB credentials: Refuse. Set
sources.db.enabled: false. Stop.
- Adapter not registered for a chosen provider: Warn, record the choice, continue. The runtime gate (Phase 4) refuses to run until the adapter ships.
- STRATEGY.md frontmatter is malformed but H1 is present: Use H1 as
name and surface a warning. If neither is parseable, prompt the user.
write_pulse_config returns { written: false, error } (or writePulseConfig throws directly): Report the validator error verbatim. Do not retry without user fix.
1---2name: harness-pulse3description: Harness Pulse4---5# Harness Pulse67> Single-page time-windowed product pulse. **Phase 3 ships the first-run interview only**: it converts vague intent into a concrete `pulse:` block in `harness.config.json`, refuses read-write DB credentials, and seeds from `STRATEGY.md` when present. The actual `harness pulse run` (Phases 2-4 of the runtime) is deferred to spec Phase 4.89## When to Use1011- Manually, when a project wants to start receiving daily pulse reports12- When `harness.config.json` has no `pulse:` block and the user invokes `/harness:pulse`13- NOT for ad-hoc one-off metric queries (that's the analytics tool's job)14- NOT for replacing dashboards (pulse is a single-page summary, not a metrics platform)15- NOT for projects that have not yet decided what their key metrics are (run `harness-strategy` first to write `STRATEGY.md`; pulse seeds from it)1617## Process1819### Iron Law2021**No PII reaches `harness.config.json` and no read-write DB credential is accepted.** Both are interview-time gates; both are hard refusals (no warnings, no overrides without an explicit user-typed escape).2223---2425### Phase 0: ROUTE BY CONFIG STATE26271. Read `harness.config.json`.282. If `pulse.enabled` is set (true OR false), skip directly to "Phase 2: RUN" — **deferred to spec Phase 4**. For now, surface "pulse already configured; the run path ships in Phase 4" and stop.293. Otherwise enter Phase 1.3031---3233### Phase 1: FIRST-RUN INTERVIEW3435Read `references/interview.md` for the SMART pushback rules and the READ-WRITE-DB rejection rule. Both are mandatory.36371. **Seed from STRATEGY.md.** Call `seed_pulse_from_strategy({ path: process.cwd() })` on the harness MCP server. Capture `{ name, keyMetrics, warnings }` — the server does the file read and bullet extraction internally. Fallback when the MCP server is unavailable: `node -e "import('@harness-engineering/core').then(m => console.log(JSON.stringify(m.seedFromStrategy({}))))"` — only works when `@harness-engineering/core` is resolvable from the project.38 - Surface `warnings` to the user verbatim.39 - If `name` is non-null, confirm it as the product name; otherwise prompt.40 - For each `keyMetric`, walk it through the SMART bar in step 4.41422. **Pick the lookback default.** Ask in plain text — present numbered options `["24h", "7d", "30d", "custom"]` and wait (do NOT use `emit_interaction`/`AskUserQuestion`; the human won't see it — it renders only to the model and the client collapses the call to "Called harness"). Default `24h` per spec.43443. **Identify the primary engagement event** (e.g. `session_started`). Apply the SMART bar. If the user can't name one, set `null` and add a pending entry. Record the event name in `primaryEvent`.45464. **Identify the value-realization event** (e.g. `plan_completed`). SMART bar applies. Record in `valueEvent`.47485. **Identify completion events** (zero or more). SMART bar per item. Record in `completionEvents`.49506. **Quality scoring (optional).** Ask whether the user wants quality sampling on a single dimension (e.g. "did the plan deliver value"). Default off. If enabled, set `qualityScoring: true` and record `qualityDimension`.51527. **Wire data sources.** Ask which providers are available:53 - `analytics`: posthog, amplitude, mixpanel, custom — or null54 - `tracing`: sentry, datadog, custom — or null55 - `payments`: stripe, custom — or null56 - `db`: opt-in only, with the **READ-WRITE-DB rejection rule** active57 For each non-null choice, check `getPulseAdapter(name)`. If absent, surface the "Phase 4 will ship the adapter" warning from `references/interview.md`.58598. **Walk every STRATEGY.md key metric** through SMART. Map to an event when wired; otherwise append to `pendingMetrics` (or `excludedMetrics` if explicitly skipped). Cite `STRATEGY.md` when seeding so the user understands provenance.60619. **Confirm the assembled config.** Show the user the proposed `pulse:` block; ask for confirmation.626310. **Write the config.** Call `write_pulse_config({ path: process.cwd(), config })` on the harness MCP server. The `config` argument is the assembled `PulseConfig` JSON. The MCP tool validates against `PulseConfigSchema` and refuses to touch disk on schema failure; passing the config as a JSON parameter (not through a shell) means user-supplied prose never crosses the shell tokenizer — no quoting, backtick, or `$VAR` hazard. The writer preserves all other config keys and writes `harness.config.json.bak` on first call.6465 Fallback for environments without the MCP server (only works when `@harness-engineering/core` is resolvable from the project): `echo '<json-blob>' | node -e "import('@harness-engineering/core').then(m => m.writePulseConfig(JSON.parse(require('fs').readFileSync(0, 'utf-8')), { configPath: 'harness.config.json' }))"`.666711. **Offer to register the `product-pulse` maintenance task.** Deferred: Phase 6 of the spec wires it. For now, surface "the daily 8am `product-pulse` task will be registered automatically once Phase 6 of the feedback-loops spec ships; you can also run pulse on demand with `/harness:pulse [window]` once Phase 4 ships."686912. **Run `harness validate`** to confirm the new `pulse:` block parses.7071---7273### Phase 2: RUN — deferred to spec Phase 47475Stub: when `pulse.enabled === true`, this phase will dispatch analytics/tracing/payments queries in parallel, run the SanitizeFn for each provider's response, and stash sanitized results for Phase 3. NOT YET IMPLEMENTED. The skill exits early with a "deferred to Phase 4" message if it reaches this phase.7677### Phase 3: ASSEMBLE — deferred7879### Phase 4: SAVE — deferred8081---8283### Question-File Mode (opt-in)8485The default is the interactive first-run interview above. **Question-file mode** is an opt-in variant for **async, team-reviewable** pulse configuration — useful when the metric/event decisions need review from data owners before the `pulse:` block is written. The skill writes its questions to a durable file, the human fills them in over time, and the skill reads them back. Enter it only when the human asks; never automatically. The full convention — file location, the `[Answer]:` tag, the read-back ritual, context hygiene — lives in `references/question-file-mode.md`; this section states only how it maps onto the pulse interview.86871. **WRITE.** Assemble the same questions Phase 1 would ask (lookback window; primary engagement event; value-realization event; completion events; quality-scoring opt-in; data-source wiring; and one question per `STRATEGY.md` Key metric when seeded) into `docs/pulse/interviews/pulse-questions.md` — a pulse-scoped home next to the pulse config it produces (no per-change slug) (or `.harness/sessions/<slug>/interviews/pulse-questions.md` when a session slug is known), each with an empty `[Answer]:`. **Never write a DB connection string into the file** — collect any `db` credential interactively so the READ-WRITE-DB rejection rule runs on a value that is not persisted to a committable file. Report the path and stop.882. **READ-BACK.** On the next run, re-read the file from disk and treat each filled `[Answer]:` as that question's answer. Apply the SMART bar to every proposed metric/event exactly as in interactive mode, flagging failures **in the file** under the question. Unanswered questions route to `pendingMetrics`/`null` per the normal skip handling.893. **CONTRADICTION PASS.** Run the cross-answer contradiction pass (see `references/interview.md` and `references/question-file-mode.md`) over the filled answers — most importantly the **metric ⇄ approach** category: a Key metric whose data source was never wired (e.g. "p95 latency" with no tracing source) is a contradiction, not merely a pending metric. Surface it; never auto-resolve.904. **PROCEED.** Continue to the unchanged Phase 1.9 confirm → `write_pulse_config` path. File mode changes only how answers are gathered — the PII gate, the READ-WRITE-DB rejection, schema validation, and `.bak` behavior are identical.9192## Harness Integration9394- **`harness validate`** — Run after `write_pulse_config`; the existing pulse-schema validator catches malformed blocks.95- **Harness MCP tools consumed by this skill** (canonical execution path — no project-local `@harness-engineering/core` required):96 - `seed_pulse_from_strategy({ path })` — defensive STRATEGY.md reader; returns `{ name, keyMetrics, warnings }`.97 - `write_pulse_config({ path, config })` — atomic config update with .bak. Validates against `PulseConfigSchema`.98- **`@harness-engineering/core`** primitives the MCP tools wrap (only directly relevant when developing inside the monorepo or when the MCP server is unavailable):99 - `writePulseConfig(config, { configPath })`, `seedFromStrategy({ cwd })`, `getPulseAdapter(name)` / `listPulseAdapters()` (Phase 4 populates), `PulseConfigSchema` / `PII_FIELD_DENYLIST`.100- **Boundary with `harness-strategy`** — Strategy writes `STRATEGY.md`; pulse reads it to seed. Pulse never writes to `STRATEGY.md`.101- **Boundary with `harness-observability`** — Observability designs _what_ to instrument; pulse is the read-side companion that surfaces what was instrumented.102- **Decision 6 (read-only)** — Pulse refuses read-write DB credentials. Documented in `references/interview.md`.103- **Decision 7 (PII contract)** — Every provider source must have a registered `SanitizeFn` adapter. Phase 3 ships the registry; Phase 4 ships the adapters.104105## Success Criteria106107- On a project with no `pulse:` block, the interview produces a valid `pulse:` block in `harness.config.json` with all non-pulse keys preserved.108- A `harness.config.json.bak` is written before mutation.109- A read-write DB credential is refused; the interview either accepts a read-only credential or sets `sources.db.enabled: false`.110- When `STRATEGY.md` exists, `name` and `Key metrics` seed the interview; missing/malformed STRATEGY.md soft-fails with warnings.111- `harness validate` passes after the interview completes.112113## Rationalizations to Reject114115These are common rationalizations that sound reasonable but lead to incorrect results. When you catch yourself thinking any of these, stop and follow the documented process instead.116117| Rationalization | Why It Is Wrong |118| ---------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |119| "The user pasted an admin DB connection string, but they clearly know their setup — I'll accept it." | The READ-WRITE-DB rejection is non-negotiable: no flag, no override, no "I know what I'm doing" path. Refuse, cite Decision 6, and set `sources.db.enabled: false`. A pulse report never needs write access. |120| "'Engagement' is a bit vague, but I know what they mean — I'll record it without SMART pushback." | SMART pushback is mandatory on every proposed metric/event. Silently accepting a vague name pollutes the config and produces an unmeasurable pulse. Push back until the metric names a concrete, measurable event. |121| "It's just one field — I'll hand-edit the `pulse:` block into `harness.config.json` directly." | `write_pulse_config` is the only sanctioned write path: it preserves all non-pulse keys, validates against `PulseConfigSchema`, and writes the `.bak`. A hand-edit bypasses the schema gate and can clobber sibling config. |122| "STRATEGY.md already lists the key metrics — I'll copy them into the config as-is." | Every seeded key metric must still clear the SMART bar and be mapped to a wired event. Metrics with no data source belong in `pendingMetrics`, not the live block — copying them wholesale ships a config that silently can't be computed. |123| "The config assembled cleanly — I'll skip `harness validate`." | A malformed `pulse:` block silently breaks the daily `product-pulse` task once the run path ships. Validation is a gate that must pass before exit, not an optional final touch. |124125## Examples126127### Example: greenfield (no STRATEGY.md, no existing pulse block)128129- Phase 0: route to Phase 1.130- Phase 1.1: `seedFromStrategy` returns `{ name: null, keyMetrics: [], warnings: ['STRATEGY.md not found'] }`.131- Phase 1.2-7: prompt user; collect `lookbackDefault: '24h'`, `primaryEvent: 'session_started'`, `valueEvent: 'plan_completed'`, `sources.analytics: 'posthog'` (with adapter-availability warning), `sources.db.enabled: false`.132- Phase 1.10: `write_pulse_config` writes the block; `.bak` saved.133- Phase 1.12: `harness validate` passes.134135### Example: STRATEGY.md present with 3 Key metrics136137- Phase 1.1: seed returns `{ name: 'Acme', keyMetrics: ['DAU', 'plans/week', 'p95 latency'] }`.138- Phase 1.8: walk each metric through SMART.139 - "DAU" → mapped to `session_started` count over 24h, accepted.140 - "plans/week" → mapped to `plan_completed` count over 7d, accepted (recorded as a future custom window).141 - "p95 latency" → no tracing source wired yet; lands in `pendingMetrics`.142143### Example: user offers an admin DB credential144145- Phase 1.7: user pastes `postgresql://admin:pwd@host/db`.146- Skill matches `admin` username against the rejection list; refuses; cites Decision 6.147- User declines to provide a read-only credential; skill writes `sources.db.enabled: false`.148149## Gates150151- **READ-WRITE-DB rejection is non-negotiable.** No flag, no override, no "I know what I'm doing" path. Refuse and document.152- **SMART pushback is mandatory on every proposed metric/event.** Silently accepting a vague name pollutes the corpus.153- **`write_pulse_config` (or `writePulseConfig` when invoked directly) is the only sanctioned write path.** Do not hand-edit `harness.config.json`. The writer is the layer that preserves non-pulse keys and writes the .bak.154- **`harness validate` must pass before exit.** A malformed `pulse:` block silently breaks the daily task once Phase 4 ships.155156## Escalation157158- **User insists on read-write DB credentials:** Refuse. Set `sources.db.enabled: false`. Stop.159- **Adapter not registered for a chosen provider:** Warn, record the choice, continue. The runtime gate (Phase 4) refuses to run until the adapter ships.160- **STRATEGY.md frontmatter is malformed but H1 is present:** Use H1 as `name` and surface a warning. If neither is parseable, prompt the user.161- **`write_pulse_config` returns `{ written: false, error }` (or `writePulseConfig` throws directly):** Report the validator error verbatim. Do not retry without user fix.