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,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `heyspex_submit_for_review` — grain-aware build→review close-out via the status API.
|
|
3
|
+
*
|
|
4
|
+
* Agents must not hand-edit frontmatter `status:` for close-out; PATCH
|
|
5
|
+
* `/api/specs/:id/status` is SSOT (coercion, rollup, spec events). [[HS-DIAG-DOSH]]
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { parseAcceptanceSubsections } from './alignment-gate.mjs';
|
|
9
|
+
import { readMvpModeState } from './mvp-mode.mjs';
|
|
10
|
+
import { isBelowReviewLevel, reviewLevelForGrain } from './review-rollup.mjs';
|
|
11
|
+
|
|
12
|
+
const TRACKABLE_KINDS = new Set(['task', 'epic', 'milestone', 'diagnose', 'review']);
|
|
13
|
+
const NON_CLOSE_KINDS = new Set(['research', 'capture', 'memory-lesson', 'memory-decision', 'memory-reference', 'memory-ui-pattern', 'protocol']);
|
|
14
|
+
|
|
15
|
+
/** Active review level from `.heyspex/config.yaml` MVP block (task when MVP off). */
|
|
16
|
+
export function resolveActiveReviewLevel(vaultRoot) {
|
|
17
|
+
const mvp = readMvpModeState(vaultRoot);
|
|
18
|
+
return reviewLevelForGrain(mvp?.review_grain);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Count acceptance criteria the same way the close-out gate expects (flat bullets). */
|
|
22
|
+
export function countAcceptanceCriteria(body) {
|
|
23
|
+
const { sections } = parseAcceptanceSubsections(body ?? '');
|
|
24
|
+
return sections.reduce((n, s) => n + (s.bullets?.length ?? 0), 0);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Post-close vault steps still required after the status API flip (HS-REV vs seed).
|
|
29
|
+
*/
|
|
30
|
+
export function buildCloseOutGuidance({ specKind, reviewLevel, effectiveStatus, isReviewUnit }) {
|
|
31
|
+
const grain = reviewLevel;
|
|
32
|
+
if (isReviewUnit) {
|
|
33
|
+
return {
|
|
34
|
+
review_unit: true,
|
|
35
|
+
effective_status: effectiveStatus,
|
|
36
|
+
hs_rev: 'required',
|
|
37
|
+
review_back_link: 'required',
|
|
38
|
+
change_log: 'required',
|
|
39
|
+
summary:
|
|
40
|
+
`Review unit at ${grain} grain — append change-log, file HS-REV companion, add ## Review back-link (session-close-protocol steps 2–4).`,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
review_unit: false,
|
|
45
|
+
effective_status: effectiveStatus,
|
|
46
|
+
hs_rev: 'skip',
|
|
47
|
+
review_back_link: 'skip',
|
|
48
|
+
change_log: 'review_seed_only',
|
|
49
|
+
summary:
|
|
50
|
+
`Below ${grain} review grain — no per-spec HS-REV. Append a one-line review/roll-up seed to ## Change log only (MVP milestone or steady-state epic grain).`,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function findClaimAcProgress(claimsBody, specId) {
|
|
55
|
+
const claims = Array.isArray(claimsBody?.claims) ? claimsBody.claims : [];
|
|
56
|
+
const hit = claims.find((c) => String(c?.specId ?? c?.spec_id ?? '') === specId);
|
|
57
|
+
if (!hit) return { found: false, acProgress: undefined };
|
|
58
|
+
const ac =
|
|
59
|
+
typeof hit.acProgress === 'number'
|
|
60
|
+
? hit.acProgress
|
|
61
|
+
: typeof hit.ac_progress === 'number'
|
|
62
|
+
? hit.ac_progress
|
|
63
|
+
: undefined;
|
|
64
|
+
return { found: true, acProgress: ac };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* @param {{ vaultRoot: string, consoleFetch: Function, postClaimEndpoint?: Function, args?: Record<string, unknown> }} ctx
|
|
69
|
+
*/
|
|
70
|
+
export async function executeSubmitForReview({ vaultRoot, consoleFetch, postClaimEndpoint, args = {} }) {
|
|
71
|
+
const specId = typeof args.spec_id === 'string' ? args.spec_id.trim() : '';
|
|
72
|
+
if (!specId) {
|
|
73
|
+
return { error: 'INVALID_ARGS', message: 'spec_id is required' };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const skipAcCheck = args.skip_ac_check === true;
|
|
77
|
+
const releaseClaim = args.release_claim !== false;
|
|
78
|
+
|
|
79
|
+
const specRes = await consoleFetch(`/api/specs/${encodeURIComponent(specId)}`);
|
|
80
|
+
if (!specRes.ok) {
|
|
81
|
+
return {
|
|
82
|
+
error: 'SPEC_FETCH_FAILED',
|
|
83
|
+
message: `GET /api/specs/${specId} returned ${specRes.status}`,
|
|
84
|
+
status: specRes.status,
|
|
85
|
+
console_response: specRes.body,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const spec = specRes.body?.spec ?? null;
|
|
90
|
+
if (!spec) {
|
|
91
|
+
return { error: 'NOT_FOUND', message: `Spec ${specId} not found` };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const specKind = String(spec.type ?? spec.frontmatter?.type ?? '').toLowerCase();
|
|
95
|
+
if (NON_CLOSE_KINDS.has(specKind)) {
|
|
96
|
+
return {
|
|
97
|
+
error: 'NOT_CLOSEABLE',
|
|
98
|
+
message: `${specId} has type "${specKind}" — research/memory/protocol specs do not participate in build→review close-out.`,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
if (!TRACKABLE_KINDS.has(specKind)) {
|
|
102
|
+
return {
|
|
103
|
+
error: 'UNKNOWN_KIND',
|
|
104
|
+
message: `${specId} has unrecognized type "${specKind}" — cannot submit for review.`,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const reviewLevel = resolveActiveReviewLevel(vaultRoot);
|
|
109
|
+
const isReviewUnit = !isBelowReviewLevel(specKind, reviewLevel);
|
|
110
|
+
const criteriaCount = countAcceptanceCriteria(spec.body);
|
|
111
|
+
|
|
112
|
+
let claimInfo = { found: false, acProgress: undefined };
|
|
113
|
+
const claimsRes = await consoleFetch('/api/control/active-claims');
|
|
114
|
+
if (claimsRes.ok) {
|
|
115
|
+
claimInfo = findClaimAcProgress(claimsRes.body, specId);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (!skipAcCheck && criteriaCount > 0) {
|
|
119
|
+
const progress = claimInfo.found ? claimInfo.acProgress : undefined;
|
|
120
|
+
if (progress === undefined || progress < criteriaCount) {
|
|
121
|
+
return {
|
|
122
|
+
error: 'AC_PROGRESS_INCOMPLETE',
|
|
123
|
+
message:
|
|
124
|
+
`Acceptance criteria progress is ${progress ?? 'unknown'} of ${criteriaCount}. Call heyspex_report_progress after each criterion, or pass skip_ac_check: true with a reason in notes.`,
|
|
125
|
+
criteria_count: criteriaCount,
|
|
126
|
+
ac_progress: progress ?? null,
|
|
127
|
+
claim_found: claimInfo.found,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const currentStatus = String(spec.status ?? spec.frontmatter?.status ?? '').toLowerCase();
|
|
133
|
+
if (currentStatus === 'closed') {
|
|
134
|
+
return {
|
|
135
|
+
error: 'ALREADY_CLOSED',
|
|
136
|
+
message: `${specId} is already closed.`,
|
|
137
|
+
spec_id: specId,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const statusRes = await consoleFetch(`/api/specs/${encodeURIComponent(specId)}/status`, {
|
|
142
|
+
method: 'PATCH',
|
|
143
|
+
body: { status: 'in_review' },
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
if (!statusRes.ok) {
|
|
147
|
+
return {
|
|
148
|
+
error: 'STATUS_PATCH_FAILED',
|
|
149
|
+
message: `PATCH /api/specs/${specId}/status returned ${statusRes.status}`,
|
|
150
|
+
status: statusRes.status,
|
|
151
|
+
console_response: statusRes.body,
|
|
152
|
+
attempted: { requested_status: 'in_review' },
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const transition = statusRes.body ?? {};
|
|
157
|
+
const effectiveStatus = String(
|
|
158
|
+
transition.spec?.frontmatter?.status ?? transition.spec?.status ?? 'in_review',
|
|
159
|
+
).toLowerCase();
|
|
160
|
+
const coercedTo = transition.coerced_to ?? transition.coercedTo ?? null;
|
|
161
|
+
|
|
162
|
+
let claimReleased;
|
|
163
|
+
if (releaseClaim && typeof postClaimEndpoint === 'function') {
|
|
164
|
+
const rel = await postClaimEndpoint('DELETE', specId);
|
|
165
|
+
claimReleased = rel.ok;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const guidance = buildCloseOutGuidance({
|
|
169
|
+
specKind,
|
|
170
|
+
reviewLevel,
|
|
171
|
+
effectiveStatus,
|
|
172
|
+
isReviewUnit,
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
return {
|
|
176
|
+
spec_id: specId,
|
|
177
|
+
requested_status: 'in_review',
|
|
178
|
+
effective_status: effectiveStatus,
|
|
179
|
+
coerced_to: coercedTo,
|
|
180
|
+
review_level: reviewLevel,
|
|
181
|
+
is_review_unit: isReviewUnit,
|
|
182
|
+
criteria_count: criteriaCount,
|
|
183
|
+
ac_progress: claimInfo.acProgress ?? null,
|
|
184
|
+
rolled_up: transition.rolledUp ?? transition.rolled_up ?? undefined,
|
|
185
|
+
propagated: transition.propagated ?? undefined,
|
|
186
|
+
claim_released: claimReleased,
|
|
187
|
+
close_out: guidance,
|
|
188
|
+
console_response: transition,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared `summary:` authoring contract — the single source of truth for how a
|
|
3
|
+
* coding agent should write a spec's plain-language `summary:` frontmatter field.
|
|
4
|
+
*
|
|
5
|
+
* This module is plain ESM with no imports so BOTH ends of the TS/JS boundary can
|
|
6
|
+
* consume it without drift:
|
|
7
|
+
* - the MCP server (`vendor/heyspex-mcp/cli.mjs`) imports it as a sibling `.mjs`
|
|
8
|
+
* to surface the authoring prompt inline in the `/hs-next` brief when an
|
|
9
|
+
* on-deck spec has no summary yet, and
|
|
10
|
+
* - the spex-console app (`spex-console/src/lib/agent-work-prompts.ts`) re-exports it
|
|
11
|
+
* through `MISSING_FIELD_CONTRACTS.summary` / `buildFieldFillInPrompt`, the
|
|
12
|
+
* contract its detail-drawer "fill in missing field" buttons already use.
|
|
13
|
+
*
|
|
14
|
+
* It MUST live under vendor/ (not spex-console/) because the MCP server is a separate
|
|
15
|
+
* process that must never import from spex-console/. Keeping one copy here eliminates
|
|
16
|
+
* the contract-drift class between the two app surfaces.
|
|
17
|
+
*
|
|
18
|
+
* The CANONICAL authoring procedure lives in the vault at
|
|
19
|
+
* `specs/protocols/spec-summary-authoring.md` (HS-PROTO-spec-summary-authoring),
|
|
20
|
+
* fetchable via `heyspex_get_procedure({ name: "spec-summary-authoring" })`. The
|
|
21
|
+
* `SUMMARY_CONTRACT` below is a SNAPSHOT of that procedure's "The contract" section so
|
|
22
|
+
* the generated prompt is self-contained; the prompt also tells the agent to fetch the
|
|
23
|
+
* latest version. When the procedure changes, update this snapshot in the same change so
|
|
24
|
+
* the two never drift. Wording policy is owned by
|
|
25
|
+
* [[HS-EPIC-Q3HK-spec-frontmatter-summary-field-policy-an]] /
|
|
26
|
+
* [[HS-TASK-W8FN-spec-summary-schema-and-protocol-docs]].
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The contract a filled-in `summary:` value must satisfy. Markdown bullet lines —
|
|
31
|
+
* rendered under a "What good looks like" heading in the authoring prompt.
|
|
32
|
+
*/
|
|
33
|
+
/** Kinds that must carry `summary:` when filed via `heyspex_file_spec` / POST /api/specs. */
|
|
34
|
+
export const SUMMARY_REQUIRED_FILE_SPEC_TYPES = new Set(['milestone', 'epic', 'task']);
|
|
35
|
+
|
|
36
|
+
const SUMMARY_AUTHORING_MOMENT = {
|
|
37
|
+
milestone:
|
|
38
|
+
'in the planning phase when the milestone is filed (with epics — never alongside tasks)',
|
|
39
|
+
epic:
|
|
40
|
+
'in the planning phase when the epic is filed (with milestones — never alongside tasks)',
|
|
41
|
+
task:
|
|
42
|
+
"from the What's Next Launch `/hs-next` breakdown only — not during plan ingest and never co-filed with epics",
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* MCP hard-block payload when `heyspex_file_spec` omits `summary` for milestone/epic/task.
|
|
47
|
+
* @param {string} type
|
|
48
|
+
*/
|
|
49
|
+
export function buildSummaryRequiredMcpError(type) {
|
|
50
|
+
const moment =
|
|
51
|
+
SUMMARY_AUTHORING_MOMENT[type] ||
|
|
52
|
+
'at the same time as the rest of the spec is authored';
|
|
53
|
+
return {
|
|
54
|
+
error: 'summary_required',
|
|
55
|
+
message: `type "${type}" requires a plain-language \`summary\` ${moment} — pass the \`summary\` parameter (or frontmatter.summary) per spec-summary-authoring. Fetch the contract: heyspex_get_procedure({ name: "spec-summary-authoring" }).`,
|
|
56
|
+
procedure: 'spec-summary-authoring',
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Soft API warning when POST /api/specs creates milestone/epic/task without summary.
|
|
62
|
+
* @param {string} type
|
|
63
|
+
*/
|
|
64
|
+
export function buildSummaryMissingApiWarning(type) {
|
|
65
|
+
const moment =
|
|
66
|
+
SUMMARY_AUTHORING_MOMENT[type] ||
|
|
67
|
+
'when the spec is authored';
|
|
68
|
+
return {
|
|
69
|
+
code: 'summary_missing',
|
|
70
|
+
message: `type "${type}" was filed without a plain-language \`summary:\` — author one ${moment} per spec-summary-authoring (heyspex_get_procedure({ name: "spec-summary-authoring" })).`,
|
|
71
|
+
procedure: 'spec-summary-authoring',
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Render `summary:` as a YAML block scalar for hand-built frontmatter writers.
|
|
77
|
+
* @param {string} summary
|
|
78
|
+
* @returns {string|null}
|
|
79
|
+
*/
|
|
80
|
+
export function formatSummaryFrontmatterField(summary) {
|
|
81
|
+
const trimmed = String(summary || '').trim();
|
|
82
|
+
if (!trimmed) return null;
|
|
83
|
+
const lines = trimmed.split(/\r?\n/);
|
|
84
|
+
return `summary: |\n${lines.map((l) => ` ${l}`).join('\n')}`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Resolve a non-empty summary from a top-level field and/or nested override.
|
|
89
|
+
* @param {unknown} topLevel
|
|
90
|
+
* @param {unknown} [alt]
|
|
91
|
+
* @returns {string|null}
|
|
92
|
+
*/
|
|
93
|
+
export function resolveAuthoredSummaryValue(topLevel, alt) {
|
|
94
|
+
const top = typeof topLevel === 'string' ? topLevel.trim() : '';
|
|
95
|
+
const nested = typeof alt === 'string' ? alt.trim() : '';
|
|
96
|
+
return top || nested || null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export const SUMMARY_CONTRACT = `- The **opening sentence** must be ONE complete, standalone sentence (target ≤160 chars) — list rows and cards render only this sentence.
|
|
100
|
+
- **If this spec has a parent** (a task's parent epic, an epic's parent milestone): the opening sentence must convey two things in one plain sentence — what THIS spec delivers (the *what*) AND the parent's goal it serves (the *why*). A reader should grasp both from that one sentence. Put them in whichever order reads naturally when you say it out loud — often the easiest is the plain action first, then the goal it serves. Do NOT use a rigid "So that …, this …" template, and do NOT stack a long purpose clause in front of the subject before the reader knows what the thing is.
|
|
101
|
+
- **If this spec has no parent** (a top-level milestone): the opening sentence is a standalone, plain-language summary of the spec itself.
|
|
102
|
+
- The **full value** may add a sentence or two of nuance/context after the opening (target ≤400 chars total).
|
|
103
|
+
- Write the WHOLE summary in plain, everyday language a twelve-year-old who has never heard of this project could understand — including the parent's goal. No project-invented jargon anywhere; no unnecessary tech/AI words (e.g. "moat", "seam", "wedge"); no examples, metaphors, or analogies. Only reach for a technical or project term when there is genuinely no plain word for the thing.
|
|
104
|
+
- Write it as a YAML block scalar (\`summary: |\`) so multi-line prose stays readable.
|
|
105
|
+
- Derive this spec's contribution from its own body and the parent's goal from the parent context provided — do not invent scope that isn't in the files.
|
|
106
|
+
- **This is a snapshot of the canonical summary-authoring procedure.** Before writing, fetch the authoritative, latest version and follow it if it has changed: \`heyspex_get_procedure({ name: "spec-summary-authoring" })\` (or read \`specs/protocols/spec-summary-authoring.md\`). Field policy: [[HS-EPIC-Q3HK-spec-frontmatter-summary-field-policy-an]] / [[HS-TASK-W8FN-spec-summary-schema-and-protocol-docs]].`;
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Build a deterministic prompt instructing a coding agent to add a missing
|
|
110
|
+
* `summary:` frontmatter field to a named spec. Mirrors the structure of the
|
|
111
|
+
* generic `buildFieldFillInPrompt` in spex-console, specialized to `summary` (the only
|
|
112
|
+
* registered field contract today).
|
|
113
|
+
*
|
|
114
|
+
* When `parent` is supplied (the spec's immediate parent — a task's epic, an epic's
|
|
115
|
+
* milestone), the prompt instructs the agent to fuse the parent's goal into the opening
|
|
116
|
+
* sentence so the summary reads in context of the work it serves. When `parent` is null
|
|
117
|
+
* (a top-level milestone) the prompt asks for a standalone summary of the spec itself.
|
|
118
|
+
*
|
|
119
|
+
* @param {{ specId: string, name: string, type: string, relPath: string,
|
|
120
|
+
* parent?: { id: string, kind?: string, title?: string, goal?: string|null, relPath?: string|null } | null }} input
|
|
121
|
+
* @returns {string}
|
|
122
|
+
*/
|
|
123
|
+
export function buildSummaryFillInPrompt({ specId, name, type, relPath, parent = null }) {
|
|
124
|
+
const parentKind = (parent && parent.kind) || 'parent';
|
|
125
|
+
const parentBlock =
|
|
126
|
+
parent && parent.id
|
|
127
|
+
? `
|
|
128
|
+
|
|
129
|
+
## Parent context
|
|
130
|
+
This ${type} rolls up to ${parentKind} **${parent.id}**${parent.title ? ` — ${parent.title}` : ''}.
|
|
131
|
+
${
|
|
132
|
+
parent.goal && String(parent.goal).trim()
|
|
133
|
+
? `Its goal, in the parent's own words:\n> ${String(parent.goal).replace(/\s+/g, ' ').trim()}`
|
|
134
|
+
: `The parent has no \`summary:\` yet${parent.relPath ? ` — open \`${parent.relPath}\` and read its \`## Goal\` / body` : ''} to state the parent's goal in plain words.`
|
|
135
|
+
}
|
|
136
|
+
Your opening sentence MUST fuse that goal with what THIS ${type} delivers toward it, in whichever order reads naturally when said out loud (no rigid "So that …, this …" template, no long purpose clause stacked in front of the subject). Keep it plain enough for a twelve-year-old who's never heard of this project.`
|
|
137
|
+
: '';
|
|
138
|
+
const readStep = parent && parent.id
|
|
139
|
+
? `1. Open \`${relPath}\` and read the spec body to understand its scope, and review the parent context above so your opening sentence carries the parent's goal.`
|
|
140
|
+
: `1. Open \`${relPath}\` and read the spec body to understand its scope.`;
|
|
141
|
+
return `You are a coding agent working in the HeySpex workflow-protocol repository. The system of record is the git-tracked markdown vault under specs/.
|
|
142
|
+
|
|
143
|
+
## Goal
|
|
144
|
+
The vault spec **${specId}** (${name}) is missing a \`summary:\` frontmatter field. Add it.
|
|
145
|
+
|
|
146
|
+
## Target spec
|
|
147
|
+
- **Spec ID:** ${specId}
|
|
148
|
+
- **Title:** ${name}
|
|
149
|
+
- **Entity type:** ${type}
|
|
150
|
+
- **Vault path:** ${relPath}
|
|
151
|
+
- **Missing field:** \`summary\`${parentBlock}
|
|
152
|
+
|
|
153
|
+
## What good looks like
|
|
154
|
+
${SUMMARY_CONTRACT}
|
|
155
|
+
|
|
156
|
+
## What to do
|
|
157
|
+
${readStep}
|
|
158
|
+
2. Add a \`summary:\` field to the YAML frontmatter that satisfies the contract above. Do **not** modify any other frontmatter field or the body.
|
|
159
|
+
3. Keep frontmatter valid YAML and preserve the existing field ordering/style of sibling specs.
|
|
160
|
+
|
|
161
|
+
Make only this one change; ask only if the spec body is too thin to summarize honestly.`;
|
|
162
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared test helpers for the vendor MCP round-trip suites.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { cpSync, existsSync, mkdirSync } from 'node:fs';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Mirror into a temp vault every real-repo file that
|
|
10
|
+
* `scripts/validate-vault-ids.mjs` needs to run in isolation.
|
|
11
|
+
*
|
|
12
|
+
* The validator resolves the rule set from `scripts/`'s sibling spex-console module
|
|
13
|
+
* `spex-console/src/lib/indexer/vault-rules.mjs` via a path relative to its OWN
|
|
14
|
+
* location (the VRUL shared-rule-engine seam — one rule set, two consumers).
|
|
15
|
+
* When a test copies only `scripts/` into a temp dir and runs the copied script,
|
|
16
|
+
* that relative import resolves to `<temp>/spex-console/src/lib/indexer/vault-rules.mjs`
|
|
17
|
+
* — which must exist or the import dangles with MODULE_NOT_FOUND. `vault-rules.mjs`
|
|
18
|
+
* additionally imports its sibling `vault-naming-rules.generated.mjs` (the ZGWR
|
|
19
|
+
* id-suffix-shape rule, compiled from protocol frontmatter), so that file must be
|
|
20
|
+
* mirrored too or the engine import dangles.
|
|
21
|
+
*
|
|
22
|
+
* Single source of truth: the next dependency the validator grows is a one-line
|
|
23
|
+
* add here, not a silent breakage in every round-trip test.
|
|
24
|
+
*
|
|
25
|
+
* @param {string} repoRoot Absolute path to the real repo root.
|
|
26
|
+
* @param {string} root Absolute path to the temp vault being assembled.
|
|
27
|
+
*/
|
|
28
|
+
export function mirrorValidatorDeps(repoRoot, root) {
|
|
29
|
+
cpSync(join(repoRoot, 'scripts'), join(root, 'scripts'), { recursive: true });
|
|
30
|
+
|
|
31
|
+
const indexerRel = join('spex-console', 'src', 'lib', 'indexer');
|
|
32
|
+
mkdirSync(join(root, indexerRel), { recursive: true });
|
|
33
|
+
for (const file of [
|
|
34
|
+
'vault-rules.mjs',
|
|
35
|
+
'vault-naming-rules.generated.mjs',
|
|
36
|
+
// validate-vault-ids.mjs imports handoff-envelope.mjs (split/defer lineage rule).
|
|
37
|
+
'handoff-envelope.mjs',
|
|
38
|
+
]) {
|
|
39
|
+
const rel = join(indexerRel, file);
|
|
40
|
+
const src = join(repoRoot, rel);
|
|
41
|
+
if (existsSync(src)) cpSync(src, join(root, rel));
|
|
42
|
+
}
|
|
43
|
+
}
|