heyspex 0.1.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/README.md +157 -0
- package/bin/hs.mjs +1028 -0
- package/bin/postinstall.mjs +88 -0
- package/dev-factory/lib/fleet-readiness.mjs +274 -0
- package/dev-factory/lib/fleet-state.mjs +250 -0
- package/dev-factory/lib/sufficiency-score.mjs +441 -0
- package/dist/cli/hs-check.mjs +747 -0
- package/dist/cli/hs-claim.mjs +541 -0
- package/dist/cli/hs-connect.mjs +3436 -0
- package/dist/cli/hs-defer.mjs +8455 -0
- package/dist/cli/hs-env.mjs +36845 -0
- package/dist/cli/hs-init.mjs +1145 -0
- package/dist/cli/hs-install.mjs +1390 -0
- package/dist/cli/hs-join.mjs +38553 -0
- package/dist/cli/hs-vault-pull.mjs +7782 -0
- package/dist/cli/hs-vault.mjs +4166 -0
- package/dist/cli/hs-workspace.mjs +2898 -0
- package/dist/cli-templates/customer-vault/specs/entity-model.md +1735 -0
- package/dist/cli-templates/customer-vault/specs/protocols/canonical-frontmatter-templates.md +531 -0
- package/dist/cli-templates/customer-vault/specs/protocols/memory-protocol.md +48 -0
- package/dist/cli-templates/customer-vault/specs/protocols/spec-hierarchy.md +548 -0
- package/docs/mcp-client-config.md +200 -0
- package/package.json +124 -0
- package/scripts/check-mcp-freshness.mjs +379 -0
- package/scripts/dev-server-status.mjs +722 -0
- package/scripts/gateway-skills-lib.mjs +421 -0
- package/scripts/lib/console-url-resolver.mjs +152 -0
- package/scripts/lib/dev-mode.mjs +369 -0
- package/scripts/lib/hs-link.mjs +154 -0
- package/scripts/lib/hs-source-commit.mjs +207 -0
- package/scripts/lib/hs-source-log.mjs +366 -0
- package/scripts/lib/hs-source-preflight.mjs +402 -0
- package/scripts/lib/hs-source-push.mjs +307 -0
- package/scripts/lib/hs-source-secret-scan.mjs +263 -0
- package/scripts/lib/standalone-doctor.mjs +89 -0
- package/scripts/vault-id-lib.mjs +173 -0
- package/vendor/heyspex-mcp/alignment-gate.mjs +1347 -0
- package/vendor/heyspex-mcp/blocker-resolver.mjs +891 -0
- package/vendor/heyspex-mcp/breakdown-ladder.mjs +309 -0
- package/vendor/heyspex-mcp/breakdown-source.mjs +236 -0
- package/vendor/heyspex-mcp/breakdown-writer.mjs +524 -0
- package/vendor/heyspex-mcp/brief-observability.mjs +67 -0
- package/vendor/heyspex-mcp/canvas-inbox.mjs +130 -0
- package/vendor/heyspex-mcp/capture-active-rehydrate.mjs +76 -0
- package/vendor/heyspex-mcp/capture-buffer-patches.mjs +147 -0
- package/vendor/heyspex-mcp/capture-channel.mjs +577 -0
- package/vendor/heyspex-mcp/capture-console-client.mjs +290 -0
- package/vendor/heyspex-mcp/capture-focus.mjs +1366 -0
- package/vendor/heyspex-mcp/capture-kind-suggest.mjs +121 -0
- package/vendor/heyspex-mcp/capture-outbox.mjs +239 -0
- package/vendor/heyspex-mcp/capture-stage-gate.mjs +160 -0
- package/vendor/heyspex-mcp/cli.mjs +15754 -0
- package/vendor/heyspex-mcp/config-settings.mjs +419 -0
- package/vendor/heyspex-mcp/console-fetch.mjs +342 -0
- package/vendor/heyspex-mcp/defer-spec.mjs +527 -0
- package/vendor/heyspex-mcp/deferred-reclaim.mjs +284 -0
- package/vendor/heyspex-mcp/design-tools.mjs +449 -0
- package/vendor/heyspex-mcp/dev-factory-bridge.mjs +66 -0
- package/vendor/heyspex-mcp/diagnose-skeleton.mjs +114 -0
- package/vendor/heyspex-mcp/fleet-queue-tools.mjs +241 -0
- package/vendor/heyspex-mcp/hooks-engine.mjs +399 -0
- package/vendor/heyspex-mcp/login-handoff.mjs +438 -0
- package/vendor/heyspex-mcp/mcp-subprocess.mjs +136 -0
- package/vendor/heyspex-mcp/mvp-mode.mjs +101 -0
- package/vendor/heyspex-mcp/next-resolution.mjs +240 -0
- package/vendor/heyspex-mcp/next-review.mjs +548 -0
- package/vendor/heyspex-mcp/peer-presence.mjs +430 -0
- package/vendor/heyspex-mcp/plan-ground-conflicts.mjs +321 -0
- package/vendor/heyspex-mcp/plan-ground.mjs +2001 -0
- package/vendor/heyspex-mcp/plan-ingest.mjs +476 -0
- package/vendor/heyspex-mcp/plan-push.mjs +1826 -0
- package/vendor/heyspex-mcp/plan-review.mjs +1419 -0
- package/vendor/heyspex-mcp/preflight-gate.mjs +0 -0
- package/vendor/heyspex-mcp/procedure-tools.mjs +970 -0
- package/vendor/heyspex-mcp/promote-capture.mjs +1280 -0
- package/vendor/heyspex-mcp/promote-diagnose.mjs +187 -0
- package/vendor/heyspex-mcp/promote-research-to-diagnose.mjs +338 -0
- package/vendor/heyspex-mcp/promote-research.mjs +1553 -0
- package/vendor/heyspex-mcp/promotion-helpers.mjs +280 -0
- package/vendor/heyspex-mcp/read-path.mjs +138 -0
- package/vendor/heyspex-mcp/recipe-tools.mjs +258 -0
- package/vendor/heyspex-mcp/regen-tasks.mjs +326 -0
- package/vendor/heyspex-mcp/register-spec.mjs +417 -0
- package/vendor/heyspex-mcp/research-focus.mjs +476 -0
- package/vendor/heyspex-mcp/research-tools.mjs +109 -0
- package/vendor/heyspex-mcp/research-type-suggest.mjs +112 -0
- package/vendor/heyspex-mcp/resolution-focus.mjs +45 -0
- package/vendor/heyspex-mcp/review-focus.mjs +55 -0
- package/vendor/heyspex-mcp/review-rollup.mjs +47 -0
- package/vendor/heyspex-mcp/run-front.mjs +60 -0
- package/vendor/heyspex-mcp/scheduler.mjs +813 -0
- package/vendor/heyspex-mcp/seat-identity.mjs +281 -0
- package/vendor/heyspex-mcp/settings-tools.mjs +119 -0
- package/vendor/heyspex-mcp/spec-id-lib.mjs +790 -0
- package/vendor/heyspex-mcp/split-research.mjs +832 -0
- package/vendor/heyspex-mcp/submit-for-review.mjs +190 -0
- package/vendor/heyspex-mcp/summary-contract.mjs +162 -0
- package/vendor/heyspex-mcp/test-helpers.mjs +43 -0
- package/vendor/heyspex-mcp/validate-linkage.mjs +574 -0
- package/vendor/heyspex-mcp/vault-resolve.mjs +297 -0
- package/vendor/heyspex-mcp/vault-settings.mjs +87 -0
- package/vendor/heyspex-mcp/verify-reproduction.mjs +314 -0
- package/vendor/heyspex-mcp/why-now.mjs +64 -0
- package/vendor/heyspex-mcp/yaml-scalar.mjs +34 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// heyspex_canvas_inbox — agent-pull side of the session-mirror write-back
|
|
2
|
+
// ([[HS-1ROV-EPIC]] stage 5c). An agent that polls calls this tool to claim any
|
|
3
|
+
// pending HeySpex canvas messages for its session. The messages are claimed
|
|
4
|
+
// THROUGH the companion daemon's localhost listener (`POST /outbox/claim`), which
|
|
5
|
+
// dequeues from the daemon's single MirrorOutbox — the same queue the Stop-hook
|
|
6
|
+
// drain and the idle injector pull from. That single ledger is what makes
|
|
7
|
+
// delivery exactly-once across all three channels: a message this tool claims is
|
|
8
|
+
// removed from the outbox, so the Stop drain / idle injection can never also
|
|
9
|
+
// deliver it (and vice-versa).
|
|
10
|
+
//
|
|
11
|
+
// Daemon down / not running / unreachable are NORMAL states (Chrome closed, no
|
|
12
|
+
// companion) — handled gracefully as an empty result with an honest note, never
|
|
13
|
+
// an error.
|
|
14
|
+
import fs from 'node:fs';
|
|
15
|
+
import path from 'node:path';
|
|
16
|
+
import http from 'node:http';
|
|
17
|
+
|
|
18
|
+
const POST_TIMEOUT_MS = 2000;
|
|
19
|
+
|
|
20
|
+
/** Read `mirrorListenerPort` from the companion settings file. 0 = no listener. */
|
|
21
|
+
export function readMirrorListenerPort(settingsPath) {
|
|
22
|
+
try {
|
|
23
|
+
const raw = fs.readFileSync(settingsPath, 'utf8');
|
|
24
|
+
const parsed = JSON.parse(raw);
|
|
25
|
+
const port = parsed?.mirrorListenerPort;
|
|
26
|
+
return Number.isInteger(port) && port > 0 ? port : 0;
|
|
27
|
+
} catch {
|
|
28
|
+
return 0;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Resolve the companion settings path. Prefers the HEYSPEX_COMPANION_SETTINGS env
|
|
34
|
+
* override; otherwise the in-repo desktop install location
|
|
35
|
+
* (`<vaultRoot>/companion/companion.settings.json`). This is the ONE file
|
|
36
|
+
* that carries `mirrorListenerPort` — NOT `spex-console/companion-settings.json`.
|
|
37
|
+
*/
|
|
38
|
+
export function resolveCompanionSettingsPath(vaultRoot) {
|
|
39
|
+
const env = process.env.HEYSPEX_COMPANION_SETTINGS;
|
|
40
|
+
if (env && env.trim().length > 0) return env.trim();
|
|
41
|
+
return path.join(vaultRoot ?? '.', 'companion', 'companion.settings.json');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Default localhost POST to the companion listener; resolves the body text. */
|
|
45
|
+
function defaultPost(port, urlPath, body) {
|
|
46
|
+
return new Promise((resolve, reject) => {
|
|
47
|
+
const payload = JSON.stringify(body);
|
|
48
|
+
const req = http.request(
|
|
49
|
+
{
|
|
50
|
+
method: 'POST',
|
|
51
|
+
hostname: '127.0.0.1',
|
|
52
|
+
port,
|
|
53
|
+
path: urlPath,
|
|
54
|
+
headers: {
|
|
55
|
+
'content-type': 'application/json',
|
|
56
|
+
'content-length': Buffer.byteLength(payload),
|
|
57
|
+
},
|
|
58
|
+
timeout: POST_TIMEOUT_MS,
|
|
59
|
+
},
|
|
60
|
+
(res) => {
|
|
61
|
+
let out = '';
|
|
62
|
+
res.setEncoding('utf8');
|
|
63
|
+
res.on('data', (c) => {
|
|
64
|
+
out += c;
|
|
65
|
+
});
|
|
66
|
+
res.on('end', () => resolve(out));
|
|
67
|
+
res.on('error', reject);
|
|
68
|
+
},
|
|
69
|
+
);
|
|
70
|
+
req.on('error', reject);
|
|
71
|
+
req.on('timeout', () => {
|
|
72
|
+
req.destroy();
|
|
73
|
+
reject(new Error('timeout'));
|
|
74
|
+
});
|
|
75
|
+
req.write(payload);
|
|
76
|
+
req.end();
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Claim pending canvas messages for `sessionId`. `postImpl(port, urlPath, body)`
|
|
82
|
+
* is injectable for tests (returns the response body text). Never throws.
|
|
83
|
+
*/
|
|
84
|
+
export async function executeCanvasInbox({ vaultRoot, sessionId, settingsPath, postImpl } = {}) {
|
|
85
|
+
const sid = typeof sessionId === 'string' ? sessionId.trim() : '';
|
|
86
|
+
if (!sid) {
|
|
87
|
+
return {
|
|
88
|
+
ok: false,
|
|
89
|
+
error: 'NO_SESSION',
|
|
90
|
+
message:
|
|
91
|
+
'A session id is required. Pass session_id, or run inside a Claude Code session ' +
|
|
92
|
+
'(CLAUDE_CODE_SESSION_ID).',
|
|
93
|
+
messages: [],
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const sp = settingsPath || resolveCompanionSettingsPath(vaultRoot);
|
|
98
|
+
const port = readMirrorListenerPort(sp);
|
|
99
|
+
if (!port) {
|
|
100
|
+
return {
|
|
101
|
+
ok: true,
|
|
102
|
+
messages: [],
|
|
103
|
+
note: 'The HeySpex companion daemon is not running (no listener port); no canvas messages.',
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const post = postImpl || defaultPost;
|
|
108
|
+
let raw;
|
|
109
|
+
try {
|
|
110
|
+
raw = await post(port, '/outbox/claim', { session_id: sid });
|
|
111
|
+
} catch {
|
|
112
|
+
return {
|
|
113
|
+
ok: true,
|
|
114
|
+
messages: [],
|
|
115
|
+
note: 'The HeySpex companion daemon is unreachable; no canvas messages.',
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
let parsed = null;
|
|
120
|
+
try {
|
|
121
|
+
parsed = JSON.parse(raw);
|
|
122
|
+
} catch {
|
|
123
|
+
parsed = null;
|
|
124
|
+
}
|
|
125
|
+
const messages = Array.isArray(parsed?.messages)
|
|
126
|
+
? parsed.messages.filter((m) => typeof m === 'string' && m.length > 0)
|
|
127
|
+
: [];
|
|
128
|
+
|
|
129
|
+
return { ok: true, sessionId: sid, delivered: messages.length, messages };
|
|
130
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve the content to report for the no-`spec_id` `heyspex_active_capture`
|
|
3
|
+
* path — extracted from cli.mjs so the decision is unit-testable without
|
|
4
|
+
* booting the MCP stdio server (mirrors the `capture-buffer-patches.mjs`
|
|
5
|
+
* factoring).
|
|
6
|
+
*
|
|
7
|
+
* Fixes [[HS-U29N-DIAG-T2DR-heyspex-active-capture-with-no-spec-id]]: the
|
|
8
|
+
* no-`spec_id` path attaches to whichever `capture_sessions` row was most
|
|
9
|
+
* recently active and used to trust its `content_buffer` at face value. A
|
|
10
|
+
* session whose capture is already materialized (`specPath` set) can carry a
|
|
11
|
+
* stale/empty `content_buffer` even though the FILE on disk holds the
|
|
12
|
+
* owner's real content — the DB buffer and the file can diverge (e.g. the
|
|
13
|
+
* file was authored/edited by a path that never PATCHed this session's
|
|
14
|
+
* buffer). Reporting the empty buffer as "this capture is empty" let an
|
|
15
|
+
* agent conclude it was safe to write fresh content, silently overwriting a
|
|
16
|
+
* live capture. This actually happened on 2026-07-13 to `HS-DN7D-CAP` and
|
|
17
|
+
* had to be restored from git.
|
|
18
|
+
*
|
|
19
|
+
* Two rules encoded here:
|
|
20
|
+
* 1. **Never trust an empty buffer when a spec file exists.** `specPath`
|
|
21
|
+
* set means this capture is materialized — a real file exists. If the
|
|
22
|
+
* buffer looks empty, read the file before concluding "empty".
|
|
23
|
+
* 2. **`is_new` is only true for a genuinely fresh capture** — no buffer
|
|
24
|
+
* content AND no materialized file. Every other case (buffer has
|
|
25
|
+
* content, or the file has content) is `is_new: false`, even when
|
|
26
|
+
* rehydration was needed to see that content.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @param {object} session - the capture_sessions row: { contentBuffer, specPath, id, ... }
|
|
31
|
+
* @param {object} deps
|
|
32
|
+
* @param {(contentBuffer: object) => { core: string, summary: string, notes: string }} deps.normalizeBuffer -
|
|
33
|
+
* normalize a content_buffer (flat or structured shape) into plain text fields.
|
|
34
|
+
* @param {(specPath: string) => ({ body: string } | null)} deps.loadSpec - load a spec file by path; null if unreadable/missing.
|
|
35
|
+
* @param {(body: string) => string} deps.extractCore - extract the `# Core Idea` section from a spec body.
|
|
36
|
+
* @param {(body: string) => string} deps.extractNotes - extract the `## Notes` section from a spec body.
|
|
37
|
+
* @returns {{ contentBuffer: object, is_new: boolean, rehydrated_from_disk: boolean }}
|
|
38
|
+
*/
|
|
39
|
+
export function resolveActiveCaptureContent(session, deps) {
|
|
40
|
+
const rawBuffer = (session && session.contentBuffer) || {};
|
|
41
|
+
const norm = deps.normalizeBuffer(rawBuffer);
|
|
42
|
+
const bufferHasContent = Boolean(norm.core || norm.summary || norm.notes);
|
|
43
|
+
|
|
44
|
+
if (bufferHasContent) {
|
|
45
|
+
// Buffer already carries real content — nothing to rehydrate, and this
|
|
46
|
+
// is by definition not a fresh/empty capture.
|
|
47
|
+
return { contentBuffer: rawBuffer, is_new: false, rehydrated_from_disk: false };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const specPath = session && session.specPath;
|
|
51
|
+
if (!specPath) {
|
|
52
|
+
// No buffer content and no materialized file → genuinely new/empty.
|
|
53
|
+
return { contentBuffer: rawBuffer, is_new: true, rehydrated_from_disk: false };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Buffer looks empty but the capture is materialized — read the file
|
|
57
|
+
// before trusting "empty". This is the exact clobber precondition from
|
|
58
|
+
// the incident: never skip this check.
|
|
59
|
+
const spec = deps.loadSpec(specPath);
|
|
60
|
+
if (spec) {
|
|
61
|
+
const core = deps.extractCore(spec.body) || '';
|
|
62
|
+
const notes = deps.extractNotes(spec.body) || '';
|
|
63
|
+
if (core || notes) {
|
|
64
|
+
return {
|
|
65
|
+
contentBuffer: { ...rawBuffer, core_idea: core, notes },
|
|
66
|
+
is_new: false,
|
|
67
|
+
rehydrated_from_disk: true,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Materialized but genuinely empty on disk too (or the file is
|
|
73
|
+
// unreadable) — still NOT a fresh capture; a spec file already exists for
|
|
74
|
+
// this id, so a caller must not treat it as safe to write from scratch.
|
|
75
|
+
return { contentBuffer: rawBuffer, is_new: false, rehydrated_from_disk: false };
|
|
76
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build the `content_buffer` PATCH set for `heyspex_update_capture` — extracted
|
|
3
|
+
* from cli.mjs so the shape contract is unit-testable without booting the MCP
|
|
4
|
+
* stdio server (mirrors the console-fetch.mjs / capture-console-client.mjs
|
|
5
|
+
* factoring).
|
|
6
|
+
*
|
|
7
|
+
* Root cause this fixes ([[HS-CAP-BX1J]] empty-body bug, sibling of
|
|
8
|
+
* [[HS-TASK-CPLO]] / [[HS-TASK-MERC]] under [[HS-EPIC-CPMR]]): the tool used to
|
|
9
|
+
* emit flat strings (`user_quotes`/`core_idea`/`concept`/`agent_expansion`)
|
|
10
|
+
* which the console persisted verbatim but every downstream reader
|
|
11
|
+
* (`hasRealContent`, `renderCaptureBody`) rejected — they require the canonical
|
|
12
|
+
* STRUCTURED `CaptureContentBuffer` shapes (see console `types.ts`):
|
|
13
|
+
* - userQuotes: Array<{ at, text }>
|
|
14
|
+
* - coreIdea / summary / details: { at, text, confirmed }
|
|
15
|
+
* so the materialized markdown body rendered empty.
|
|
16
|
+
*
|
|
17
|
+
* Two correctness rules encoded here:
|
|
18
|
+
* 1. **Structured, camelCase paths.** `userQuotes` (array), `coreIdea`,
|
|
19
|
+
* `summary` (renamed from `concept`, HS-EPIC-OED2), and `details` (objects).
|
|
20
|
+
* `agent_expansion` is the `## Expanded Concept` deep-dive section → maps to
|
|
21
|
+
* `details`. `agent_note` has no body section in the v2 shape (Notes is
|
|
22
|
+
* user-verbatim only) → it lands in `chatTurns`, the schema's home for agent
|
|
23
|
+
* coordination.
|
|
24
|
+
* 2. **Append, not overwrite.** The console PATCH route stores top-level keys
|
|
25
|
+
* verbatim (last-write-wins), so multi-beat `user_quote` calls would clobber
|
|
26
|
+
* each other. We read the prior buffer and append, sending the full array.
|
|
27
|
+
*
|
|
28
|
+
* `confirmed: true` is set on every agent-authored section because the MCP write
|
|
29
|
+
* is an explicit commit — the agent only calls this after the conversational
|
|
30
|
+
* confirm in the /hs-capture four-beat protocol.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
/** Keep only well-formed `{ at, text }` quote entries from a prior buffer. */
|
|
34
|
+
function priorUserQuotes(currentBuffer) {
|
|
35
|
+
const raw = currentBuffer?.userQuotes ?? currentBuffer?.user_quotes;
|
|
36
|
+
if (!Array.isArray(raw)) return [];
|
|
37
|
+
return raw.filter(
|
|
38
|
+
(q) => q && typeof q === 'object' && typeof q.at === 'string' && typeof q.text === 'string',
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Keep only well-formed chat turns from a prior buffer. */
|
|
43
|
+
function priorChatTurns(currentBuffer) {
|
|
44
|
+
const raw = currentBuffer?.chatTurns ?? currentBuffer?.chat_turns;
|
|
45
|
+
if (!Array.isArray(raw)) return [];
|
|
46
|
+
return raw.filter(
|
|
47
|
+
(t) => t && typeof t === 'object' && typeof t.at === 'string' && typeof t.text === 'string',
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function collapse(value) {
|
|
52
|
+
return String(value).trim().replace(/\n+/g, ' ');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* @param {object} args - { user_quote, agent_note, core_idea, summary (alias: concept), agent_expansion, example, user_stories, kind, done_when, jtbd }
|
|
57
|
+
* @param {object} currentBuffer - the session's existing content_buffer (may be {})
|
|
58
|
+
* @param {string} iso - ISO timestamp to stamp on the new entries
|
|
59
|
+
* @returns {Array<{ path: string, value: unknown }>} canonical structured patches
|
|
60
|
+
*/
|
|
61
|
+
export function buildCaptureBufferPatches(args = {}, currentBuffer = {}, iso) {
|
|
62
|
+
const patches = [];
|
|
63
|
+
|
|
64
|
+
if (args.user_quote !== undefined && collapse(args.user_quote)) {
|
|
65
|
+
const next = [...priorUserQuotes(currentBuffer), { at: iso, text: collapse(args.user_quote) }];
|
|
66
|
+
patches.push({ path: 'userQuotes', value: next });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (args.agent_note !== undefined && collapse(args.agent_note)) {
|
|
70
|
+
const next = [
|
|
71
|
+
...priorChatTurns(currentBuffer),
|
|
72
|
+
{ at: iso, who: 'agent', text: collapse(args.agent_note) },
|
|
73
|
+
];
|
|
74
|
+
patches.push({ path: 'chatTurns', value: next });
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (args.core_idea !== undefined && String(args.core_idea).trim()) {
|
|
78
|
+
patches.push({ path: 'coreIdea', value: { at: iso, text: String(args.core_idea).trim(), confirmed: true } });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// `summary` is canonical; `concept` is the deprecated alias (HS-EPIC-OED2).
|
|
82
|
+
const summaryArg = args.summary ?? args.concept;
|
|
83
|
+
if (summaryArg !== undefined && String(summaryArg).trim()) {
|
|
84
|
+
patches.push({ path: 'summary', value: { at: iso, text: String(summaryArg).trim(), confirmed: true } });
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// agent_expansion is the `## Expanded Concept` deep-dive section → buffer.details.
|
|
88
|
+
if (args.agent_expansion !== undefined && String(args.agent_expansion).trim()) {
|
|
89
|
+
patches.push({ path: 'details', value: { at: iso, text: String(args.agent_expansion).trim(), confirmed: true } });
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// example is the `## Example` section (real or AI-illustrative) → buffer.example.
|
|
93
|
+
if (args.example !== undefined && String(args.example).trim()) {
|
|
94
|
+
patches.push({ path: 'example', value: { at: iso, text: String(args.example).trim(), confirmed: true } });
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// user_stories is the `## User Stories` section → buffer.userStories. Root cause
|
|
98
|
+
// of [[HS-TXBY-DIAG-W32W-heyspex-update-capture-silently-drops]]: this patch was
|
|
99
|
+
// never emitted, so the tool accepted user_stories, returned success, but the
|
|
100
|
+
// console content_buffer (and therefore the materialized file) never saw it —
|
|
101
|
+
// silent data loss, unlike example which has always had this push.
|
|
102
|
+
if (args.user_stories !== undefined && String(args.user_stories).trim()) {
|
|
103
|
+
patches.push({
|
|
104
|
+
path: 'userStories',
|
|
105
|
+
value: { at: iso, text: String(args.user_stories).trim(), confirmed: true },
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// kind re-classifies the capture's frontmatter `kind:` (idea/bug-fix/clip/
|
|
110
|
+
// reference/question). Simple scalar, not a timestamped section — mirrors
|
|
111
|
+
// the tool schema's enum contract. Root cause of
|
|
112
|
+
// [[HS-NR43-DIAG-CAIO-promote-sufficiency-gate-demands-kind]]: the tool
|
|
113
|
+
// schema already exposed `kind` but this builder never emitted a patch for
|
|
114
|
+
// it, so the argument was silently dropped and the promote sufficiency gate
|
|
115
|
+
// (which reads frontmatter.kind) was unsatisfiable through the documented
|
|
116
|
+
// tool path.
|
|
117
|
+
if (args.kind !== undefined && String(args.kind).trim()) {
|
|
118
|
+
patches.push({ path: 'kind', value: String(args.kind).trim() });
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// done_when is the `## Done When` checklist section → buffer.doneWhen, same
|
|
122
|
+
// {at,text,confirmed} shape as the other agent-authored sections (example,
|
|
123
|
+
// user_stories, details) — sibling half of the HS-NR43 fix above.
|
|
124
|
+
if (args.done_when !== undefined && String(args.done_when).trim()) {
|
|
125
|
+
patches.push({
|
|
126
|
+
path: 'doneWhen',
|
|
127
|
+
value: { at: iso, text: String(args.done_when).trim(), confirmed: true },
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// jtbd is the `## JTBD (job-to-be-done)` section → buffer.jtbd, same
|
|
132
|
+
// {at,text,confirmed} shape as example/userStories/doneWhen. Authored by the
|
|
133
|
+
// /hs-capture enhancement pass; replaces the interim Expanded-Concept-leading
|
|
134
|
+
// -block convention (owner ruling 2026-08-23) —
|
|
135
|
+
// [[HS-TIE7-PR20-TASK-jtbd-job-to-be-done-field-on-captures]]. `jtbd` is
|
|
136
|
+
// explicitly named in `UPDATE_CAPTURE_ACCEPTED_ARGS` (capture-focus.mjs) so it
|
|
137
|
+
// is refused by name at the handler's first hop, never silently dropped here
|
|
138
|
+
// — [[additionalproperties-false-in-an-mcp]].
|
|
139
|
+
if (args.jtbd !== undefined && String(args.jtbd).trim()) {
|
|
140
|
+
patches.push({
|
|
141
|
+
path: 'jtbd',
|
|
142
|
+
value: { at: iso, text: String(args.jtbd).trim(), confirmed: true },
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return patches;
|
|
147
|
+
}
|