@pathmode/mcp-server 1.7.0 → 1.9.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 +22 -7
- package/dist/api-client.d.ts +51 -1
- package/dist/api-client.d.ts.map +1 -1
- package/dist/index.d.ts +7 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +814 -246
- package/dist/intent-compiler.d.ts +36 -1
- package/dist/intent-compiler.d.ts.map +1 -1
- package/dist/local-reader.d.ts +45 -1
- package/dist/local-reader.d.ts.map +1 -1
- package/dist/pathmode-section.d.ts +22 -0
- package/dist/pathmode-section.d.ts.map +1 -0
- package/dist/save-policy.d.ts +41 -0
- package/dist/save-policy.d.ts.map +1 -0
- package/dist/setup.d.ts.map +1 -1
- package/package.json +2 -2
- package/skills/README.md +4 -1
package/dist/index.js
CHANGED
|
@@ -33598,6 +33598,39 @@ class PathmodeClient {
|
|
|
33598
33598
|
}
|
|
33599
33599
|
return response;
|
|
33600
33600
|
}
|
|
33601
|
+
/**
|
|
33602
|
+
* Announce this client to the server once at launch.
|
|
33603
|
+
*
|
|
33604
|
+
* The server otherwise hears nothing until a tool fires, so a user who installs
|
|
33605
|
+
* the connector and never invokes anything is indistinguishable from one who
|
|
33606
|
+
* never installed it at all. That ambiguity hid the shape of the biggest drop-off
|
|
33607
|
+
* in the funnel: 73% of people who created a key had no successful call, and we
|
|
33608
|
+
* could not tell whether they were stuck on setup or simply idle.
|
|
33609
|
+
*
|
|
33610
|
+
* Silent by contract. Never throws, never writes to stdout (stdout belongs to
|
|
33611
|
+
* JSON-RPC), and never blocks startup — a failed handshake must not cost the user
|
|
33612
|
+
* a working server. Deliberately does NOT go through `fetch()` above, which logs
|
|
33613
|
+
* to stderr and throws on non-2xx.
|
|
33614
|
+
*/
|
|
33615
|
+
async handshake(clientInfo = {}) {
|
|
33616
|
+
try {
|
|
33617
|
+
const res = await fetch(`${this.apiUrl}/api/v1/connection/handshake`, {
|
|
33618
|
+
method: 'POST',
|
|
33619
|
+
headers: {
|
|
33620
|
+
'Authorization': `Bearer ${this.apiKey}`,
|
|
33621
|
+
'Content-Type': 'application/json',
|
|
33622
|
+
},
|
|
33623
|
+
body: JSON.stringify(clientInfo),
|
|
33624
|
+
signal: AbortSignal.timeout(5000),
|
|
33625
|
+
});
|
|
33626
|
+
if (isDebug)
|
|
33627
|
+
console.error(`[pathmode-mcp] handshake: ${res.status}`);
|
|
33628
|
+
}
|
|
33629
|
+
catch (e) {
|
|
33630
|
+
if (isDebug)
|
|
33631
|
+
console.error(`[pathmode-mcp] handshake failed (ignored): ${e}`);
|
|
33632
|
+
}
|
|
33633
|
+
}
|
|
33601
33634
|
async listIntents(status) {
|
|
33602
33635
|
const params = status ? `?status=${status}` : '';
|
|
33603
33636
|
const res = await this.fetch(`/intents${params}`);
|
|
@@ -33626,6 +33659,13 @@ class PathmodeClient {
|
|
|
33626
33659
|
});
|
|
33627
33660
|
return res.json();
|
|
33628
33661
|
}
|
|
33662
|
+
async recordFinding(intentId, input) {
|
|
33663
|
+
const res = await this.fetch(`/intents/${intentId}/findings`, {
|
|
33664
|
+
method: 'POST',
|
|
33665
|
+
body: JSON.stringify(input),
|
|
33666
|
+
});
|
|
33667
|
+
return res.json();
|
|
33668
|
+
}
|
|
33629
33669
|
async getWorkspace() {
|
|
33630
33670
|
const res = await this.fetch('/workspace');
|
|
33631
33671
|
return res.json();
|
|
@@ -33912,6 +33952,57 @@ exports.formatIntentMd = formatIntentMd;
|
|
|
33912
33952
|
exports.formatCursorRules = formatCursorRules;
|
|
33913
33953
|
exports.formatClaudeMdSection = formatClaudeMdSection;
|
|
33914
33954
|
exports.formatOutcomeRubric = formatOutcomeRubric;
|
|
33955
|
+
const VERIFICATION_KIND_LABELS = {
|
|
33956
|
+
fastest: 'Fastest check',
|
|
33957
|
+
'shipped-signal': 'Shipped signal',
|
|
33958
|
+
'regression-guard': 'Regression guard',
|
|
33959
|
+
manual: 'Manual check',
|
|
33960
|
+
test: 'Automated test',
|
|
33961
|
+
};
|
|
33962
|
+
const VERIFICATION_KIND_ORDER = ['fastest', 'shipped-signal', 'regression-guard', 'manual', 'test'];
|
|
33963
|
+
const VERIFICATION_KIND_SET = new Set(VERIFICATION_KIND_ORDER);
|
|
33964
|
+
/** Read verification uniformly as a check collection: canonical checks[] + adapted legacy buckets. */
|
|
33965
|
+
function toVerificationChecks(v) {
|
|
33966
|
+
if (!v || typeof v !== 'object')
|
|
33967
|
+
return [];
|
|
33968
|
+
const out = [];
|
|
33969
|
+
for (const c of Array.isArray(v.checks) ? v.checks : []) {
|
|
33970
|
+
const description = typeof c?.description === 'string' ? c.description.trim() : '';
|
|
33971
|
+
if (!description)
|
|
33972
|
+
continue;
|
|
33973
|
+
out.push({
|
|
33974
|
+
kind: VERIFICATION_KIND_SET.has(c.kind) ? c.kind : 'test',
|
|
33975
|
+
description,
|
|
33976
|
+
status: c.status,
|
|
33977
|
+
verifies: typeof c.verifies === 'string' && c.verifies.trim()
|
|
33978
|
+
? c.verifies.trim()
|
|
33979
|
+
: undefined,
|
|
33980
|
+
});
|
|
33981
|
+
}
|
|
33982
|
+
const legacy = [
|
|
33983
|
+
[v.e2eTests, 'test'], [v.unitTests, 'test'], [v.manualChecks, 'manual'],
|
|
33984
|
+
];
|
|
33985
|
+
for (const [arr, kind] of legacy) {
|
|
33986
|
+
for (const s of arr ?? []) {
|
|
33987
|
+
if (typeof s === 'string' && s.trim())
|
|
33988
|
+
out.push({ kind, description: s.trim() });
|
|
33989
|
+
}
|
|
33990
|
+
}
|
|
33991
|
+
return out;
|
|
33992
|
+
}
|
|
33993
|
+
/** Group verification into ordered, non-empty kind groups for rendering. */
|
|
33994
|
+
function groupVerificationChecks(v) {
|
|
33995
|
+
const checks = toVerificationChecks(v);
|
|
33996
|
+
return VERIFICATION_KIND_ORDER
|
|
33997
|
+
.map((kind) => ({ kind, label: VERIFICATION_KIND_LABELS[kind], checks: checks.filter((c) => c.kind === kind) }))
|
|
33998
|
+
.filter((g) => g.checks.length > 0);
|
|
33999
|
+
}
|
|
34000
|
+
/** A single check rendered as agent-facing text, annotated with its verdict and verifies target. */
|
|
34001
|
+
function renderCheckLine(c) {
|
|
34002
|
+
const verifies = c.verifies ? ` (verifies: ${c.verifies})` : '';
|
|
34003
|
+
const status = c.status && c.status !== 'unknown' ? ` [${c.status}]` : '';
|
|
34004
|
+
return `${c.description}${verifies}${status}`;
|
|
34005
|
+
}
|
|
33915
34006
|
/** Extract text from a string or structured outcome. */
|
|
33916
34007
|
function getOutcomeText(o) {
|
|
33917
34008
|
return typeof o === 'string' ? o : o.text;
|
|
@@ -34023,24 +34114,41 @@ IMPORTANT:
|
|
|
34023
34114
|
- edgeCases: { scenario: string, expectedBehavior: string }[] (optional)
|
|
34024
34115
|
- healthMetrics: string[] (optional)
|
|
34025
34116
|
- scope: { inScope?: string[], outOfScope?: string[] } (optional)
|
|
34026
|
-
- verification: {
|
|
34117
|
+
- verification: { checks?: { kind: 'fastest'|'shipped-signal'|'regression-guard'|'manual'|'test', description: string }[] } (optional) — verification is a feedback loop, not just tests: a fastest check (quickest signal it works), a shipped-signal (observable production signal it landed), a regression-guard (what must not break), plus manual/test as needed. Legacy { manualChecks?, unitTests?, e2eTests? } string arrays are still accepted.
|
|
34027
34118
|
|
|
34028
34119
|
Now, start the conversation. If they haven't provided one yet, ask for the concrete evidence (quote, metric, or ticket) driving this work.`;
|
|
34029
34120
|
}
|
|
34030
34121
|
// ============================================================
|
|
34031
34122
|
// Format: intent.md
|
|
34032
34123
|
// ============================================================
|
|
34124
|
+
/** Text-only decisions lines for the local file formatters (local mode has no evidence store).
|
|
34125
|
+
* Defensive: skips entries missing a string choice/reason. Returns [] when none (incl. a leading
|
|
34126
|
+
* blank line for section spacing when present). */
|
|
34127
|
+
function decisionLines(decisions, heading) {
|
|
34128
|
+
const valid = (decisions || []).filter(d => d && typeof d.choice === 'string' && d.choice.length > 0 && typeof d.reason === 'string');
|
|
34129
|
+
if (valid.length === 0)
|
|
34130
|
+
return [];
|
|
34131
|
+
const lines = ['', heading];
|
|
34132
|
+
for (const d of valid) {
|
|
34133
|
+
lines.push(`- **${d.choice}**${d.ruledOut ? ` (instead of: ${d.ruledOut})` : ''} — ${d.reason}`);
|
|
34134
|
+
}
|
|
34135
|
+
return lines;
|
|
34136
|
+
}
|
|
34033
34137
|
/**
|
|
34034
34138
|
* Generate intent.md content with YAML frontmatter.
|
|
34035
34139
|
* Adapted from lib/agentPromptGenerator.ts generateIntentMd().
|
|
34140
|
+
*
|
|
34141
|
+
* `spec.id`, `opts.version`, and `opts.status` are preserved rather than reset — an intent that
|
|
34142
|
+
* is saved twice must keep its identity and its lifecycle state. Only a genuinely new intent
|
|
34143
|
+
* (no id) gets a minted id, version 1, and status 'draft'.
|
|
34036
34144
|
*/
|
|
34037
|
-
function formatIntentMd(spec) {
|
|
34145
|
+
function formatIntentMd(spec, opts = {}) {
|
|
34038
34146
|
const now = new Date().toISOString();
|
|
34039
34147
|
const frontmatter = {
|
|
34040
34148
|
id: spec.id || `intent_${Date.now()}`,
|
|
34041
|
-
version: 1,
|
|
34042
|
-
status: 'draft',
|
|
34043
|
-
created: now,
|
|
34149
|
+
version: opts.version && opts.version >= 1 ? opts.version : 1,
|
|
34150
|
+
status: opts.status || 'draft',
|
|
34151
|
+
created: opts.created || now,
|
|
34044
34152
|
updated: now,
|
|
34045
34153
|
};
|
|
34046
34154
|
const yamlLines = Object.entries(frontmatter)
|
|
@@ -34057,6 +34165,7 @@ function formatIntentMd(spec) {
|
|
|
34057
34165
|
sections.push('## Objective');
|
|
34058
34166
|
sections.push(spec.objective);
|
|
34059
34167
|
}
|
|
34168
|
+
sections.push(...decisionLines(spec.decisions, '## Decisions & Ruled-Out Alternatives'));
|
|
34060
34169
|
if (spec.outcomes?.length) {
|
|
34061
34170
|
sections.push('');
|
|
34062
34171
|
sections.push('## Outcomes');
|
|
@@ -34099,27 +34208,15 @@ function formatIntentMd(spec) {
|
|
|
34099
34208
|
sections.push(`- ${metric}`);
|
|
34100
34209
|
}
|
|
34101
34210
|
}
|
|
34102
|
-
|
|
34103
|
-
|
|
34104
|
-
|
|
34105
|
-
|
|
34106
|
-
|
|
34107
|
-
|
|
34108
|
-
|
|
34109
|
-
|
|
34110
|
-
|
|
34111
|
-
sections.push(`- [ ] ${t}`);
|
|
34112
|
-
}
|
|
34113
|
-
if (unitTests?.length) {
|
|
34114
|
-
sections.push('**Unit Tests**:');
|
|
34115
|
-
for (const t of unitTests)
|
|
34116
|
-
sections.push(`- [ ] ${t}`);
|
|
34117
|
-
}
|
|
34118
|
-
if (manualChecks?.length) {
|
|
34119
|
-
sections.push('**Manual Checks**:');
|
|
34120
|
-
for (const t of manualChecks)
|
|
34121
|
-
sections.push(`- [ ] ${t}`);
|
|
34122
|
-
}
|
|
34211
|
+
const intentMdChecks = groupVerificationChecks(spec.verification);
|
|
34212
|
+
if (intentMdChecks.length) {
|
|
34213
|
+
sections.push('');
|
|
34214
|
+
sections.push('## Verification');
|
|
34215
|
+
sections.push('_A feedback loop, not just a test list._');
|
|
34216
|
+
for (const g of intentMdChecks) {
|
|
34217
|
+
sections.push(`**${g.label}**:`);
|
|
34218
|
+
for (const c of g.checks)
|
|
34219
|
+
sections.push(`- [ ] ${renderCheckLine(c)}`);
|
|
34123
34220
|
}
|
|
34124
34221
|
}
|
|
34125
34222
|
sections.push('');
|
|
@@ -34146,6 +34243,7 @@ function formatCursorRules(spec) {
|
|
|
34146
34243
|
sections.push('# WHY');
|
|
34147
34244
|
sections.push(spec.objective);
|
|
34148
34245
|
}
|
|
34246
|
+
sections.push(...decisionLines(spec.decisions, '# DECISIONS (already settled — do not relitigate)'));
|
|
34149
34247
|
if (spec.outcomes?.length) {
|
|
34150
34248
|
sections.push('');
|
|
34151
34249
|
sections.push('# SUCCESS OUTCOMES');
|
|
@@ -34192,25 +34290,15 @@ function formatCursorRules(spec) {
|
|
|
34192
34290
|
sections.push(`- ${metric}`);
|
|
34193
34291
|
}
|
|
34194
34292
|
}
|
|
34195
|
-
|
|
34196
|
-
|
|
34197
|
-
|
|
34198
|
-
|
|
34199
|
-
|
|
34200
|
-
|
|
34201
|
-
sections.push(
|
|
34202
|
-
|
|
34203
|
-
|
|
34204
|
-
sections.push(`- [e2e] ${t}`);
|
|
34205
|
-
}
|
|
34206
|
-
if (unitTests?.length) {
|
|
34207
|
-
for (const t of unitTests)
|
|
34208
|
-
sections.push(`- [unit] ${t}`);
|
|
34209
|
-
}
|
|
34210
|
-
if (manualChecks?.length) {
|
|
34211
|
-
for (const t of manualChecks)
|
|
34212
|
-
sections.push(`- [manual] ${t}`);
|
|
34213
|
-
}
|
|
34293
|
+
const cursorChecks = groupVerificationChecks(spec.verification);
|
|
34294
|
+
if (cursorChecks.length) {
|
|
34295
|
+
sections.push('');
|
|
34296
|
+
sections.push('# VERIFICATION');
|
|
34297
|
+
sections.push('After implementation, verify (a feedback loop, not just tests):');
|
|
34298
|
+
for (const g of cursorChecks) {
|
|
34299
|
+
sections.push(`**${g.label}**:`);
|
|
34300
|
+
for (const c of g.checks)
|
|
34301
|
+
sections.push(`- ${renderCheckLine(c)}`);
|
|
34214
34302
|
}
|
|
34215
34303
|
}
|
|
34216
34304
|
sections.push('');
|
|
@@ -34236,6 +34324,7 @@ function formatClaudeMdSection(spec) {
|
|
|
34236
34324
|
if (spec.objective) {
|
|
34237
34325
|
sections.push(`**Objective**: ${spec.objective}`);
|
|
34238
34326
|
}
|
|
34327
|
+
sections.push(...decisionLines(spec.decisions, '**Decisions**:'));
|
|
34239
34328
|
if (spec.outcomes?.length) {
|
|
34240
34329
|
sections.push('**Outcomes**:');
|
|
34241
34330
|
sections.push(spec.outcomes.map(o => `- [ ] ${getPriorityLabel(o)}${getOutcomeText(o)}`).join('\n'));
|
|
@@ -34315,6 +34404,13 @@ function buildWriterDescription(spec) {
|
|
|
34315
34404
|
sections.push('');
|
|
34316
34405
|
sections.push(`Why this matters: ${spec.objective}`);
|
|
34317
34406
|
}
|
|
34407
|
+
const decisions = (spec.decisions || []).filter(d => d && typeof d.choice === 'string' && d.choice.length > 0 && typeof d.reason === 'string');
|
|
34408
|
+
if (decisions.length > 0) {
|
|
34409
|
+
sections.push('');
|
|
34410
|
+
sections.push('Decisions already made (do not relitigate):');
|
|
34411
|
+
for (const d of decisions)
|
|
34412
|
+
sections.push(`- ${d.choice}${d.ruledOut ? ` (instead of: ${d.ruledOut})` : ''} — ${d.reason}`);
|
|
34413
|
+
}
|
|
34318
34414
|
if (spec.scope?.inScope?.length) {
|
|
34319
34415
|
sections.push('');
|
|
34320
34416
|
sections.push('In scope:');
|
|
@@ -34399,17 +34495,9 @@ function buildGraderRubric(spec, opts = {}) {
|
|
|
34399
34495
|
sections.push(`- ${c}`);
|
|
34400
34496
|
}
|
|
34401
34497
|
// Verification → procedures the grader must run to produce evidence
|
|
34402
|
-
const v = spec.verification;
|
|
34403
34498
|
const checks = [];
|
|
34404
|
-
for (const
|
|
34405
|
-
|
|
34406
|
-
checks.push(`[e2e] ${t}`);
|
|
34407
|
-
for (const t of v?.unitTests ?? [])
|
|
34408
|
-
if (t?.trim())
|
|
34409
|
-
checks.push(`[unit] ${t}`);
|
|
34410
|
-
for (const t of v?.manualChecks ?? [])
|
|
34411
|
-
if (t?.trim())
|
|
34412
|
-
checks.push(`[manual] ${t}`);
|
|
34499
|
+
for (const c of toVerificationChecks(spec.verification))
|
|
34500
|
+
checks.push(`[${c.kind}] ${renderCheckLine(c)}`);
|
|
34413
34501
|
for (const t of spec.implementationContext?.verificationSuggestions ?? [])
|
|
34414
34502
|
if (t?.trim())
|
|
34415
34503
|
checks.push(`[suggested] ${t}`);
|
|
@@ -34451,11 +34539,7 @@ function formatOutcomeRubric(spec, opts = {}) {
|
|
|
34451
34539
|
const maxIterations = opts.maxIterations ?? 5;
|
|
34452
34540
|
const description = buildWriterDescription(spec);
|
|
34453
34541
|
const rubric = buildGraderRubric(spec, opts);
|
|
34454
|
-
const hasVerification =
|
|
34455
|
-
...(spec.verification?.e2eTests ?? []),
|
|
34456
|
-
...(spec.verification?.unitTests ?? []),
|
|
34457
|
-
...(spec.verification?.manualChecks ?? []),
|
|
34458
|
-
].some((t) => t?.trim());
|
|
34542
|
+
const hasVerification = toVerificationChecks(spec.verification).length > 0;
|
|
34459
34543
|
const doc = [];
|
|
34460
34544
|
doc.push('<!-- Pathmode → Claude Managed Agents: Outcomes rubric -->');
|
|
34461
34545
|
doc.push(`<!-- Generated ${new Date().toISOString()} | pathmode.io -->`);
|
|
@@ -34497,15 +34581,24 @@ function formatOutcomeRubric(spec, opts = {}) {
|
|
|
34497
34581
|
/**
|
|
34498
34582
|
* Local Intent Reader
|
|
34499
34583
|
* Reads intent.md files from the current working directory for offline/local mode.
|
|
34584
|
+
*
|
|
34585
|
+
* Parsing is line-based on purpose. The previous implementation extracted sections with
|
|
34586
|
+
* `/^##\s+Heading\s*\n([\s\S]*?)(?=^##\s|$)/m` — but under the `m` flag `$` matches at EVERY
|
|
34587
|
+
* line end, so the lazy quantifier stopped after the FIRST line. Every section round-tripped
|
|
34588
|
+
* as a single item: three outcomes read back as one, `## Scope` vanished entirely, and
|
|
34589
|
+
* `## Verification` produced `{}`. See round-trip.test.ts for the regression guard.
|
|
34500
34590
|
*/
|
|
34501
34591
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
34502
34592
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
34503
34593
|
};
|
|
34504
34594
|
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
|
34505
34595
|
exports.readLocalIntents = readLocalIntents;
|
|
34596
|
+
exports.readIntentMeta = readIntentMeta;
|
|
34597
|
+
exports.readIntentFile = readIntentFile;
|
|
34506
34598
|
const fs_1 = __importDefault(__nccwpck_require__(9896));
|
|
34507
34599
|
const path_1 = __importDefault(__nccwpck_require__(6928));
|
|
34508
34600
|
const gray_matter_1 = __importDefault(__nccwpck_require__(9599));
|
|
34601
|
+
const DECISIONS_HEADING = 'Decisions & Ruled-Out Alternatives';
|
|
34509
34602
|
/**
|
|
34510
34603
|
* Read all intent.md files from the current directory and subdirectories (1 level deep).
|
|
34511
34604
|
*/
|
|
@@ -34528,6 +34621,28 @@ function readLocalIntents() {
|
|
|
34528
34621
|
}
|
|
34529
34622
|
return intents;
|
|
34530
34623
|
}
|
|
34624
|
+
/**
|
|
34625
|
+
* Read only the frontmatter identity of an intent.md. Used by `intent_save` to decide whether a
|
|
34626
|
+
* write is an update (same id → bump version, keep status) or a collision (different id → refuse
|
|
34627
|
+
* rather than clobber someone else's spec).
|
|
34628
|
+
*/
|
|
34629
|
+
function readIntentMeta(filePath) {
|
|
34630
|
+
if (!fs_1.default.existsSync(filePath))
|
|
34631
|
+
return null;
|
|
34632
|
+
try {
|
|
34633
|
+
const { data } = (0, gray_matter_1.default)(fs_1.default.readFileSync(filePath, 'utf-8'));
|
|
34634
|
+
const version = Number(data.version);
|
|
34635
|
+
return {
|
|
34636
|
+
id: typeof data.id === 'string' && data.id.trim() ? data.id.trim() : null,
|
|
34637
|
+
version: Number.isFinite(version) && version >= 1 ? version : 1,
|
|
34638
|
+
status: typeof data.status === 'string' && data.status.trim() ? data.status.trim() : 'draft',
|
|
34639
|
+
created: typeof data.created === 'string' ? data.created : undefined,
|
|
34640
|
+
};
|
|
34641
|
+
}
|
|
34642
|
+
catch {
|
|
34643
|
+
return null;
|
|
34644
|
+
}
|
|
34645
|
+
}
|
|
34531
34646
|
/**
|
|
34532
34647
|
* Parse a single intent.md file with YAML frontmatter.
|
|
34533
34648
|
*/
|
|
@@ -34537,32 +34652,29 @@ function readIntentFile(filePath) {
|
|
|
34537
34652
|
try {
|
|
34538
34653
|
const content = fs_1.default.readFileSync(filePath, 'utf-8');
|
|
34539
34654
|
const { data, content: body } = (0, gray_matter_1.default)(content);
|
|
34540
|
-
|
|
34541
|
-
const
|
|
34542
|
-
const constraints = extractListSection(body, 'Constraints');
|
|
34543
|
-
const healthMetrics = extractListSection(body, 'Health Metrics');
|
|
34544
|
-
const edgeCases = extractEdgeCases(body);
|
|
34545
|
-
const scope = extractScope(body);
|
|
34546
|
-
const parsedVerification = extractVerification(body);
|
|
34655
|
+
const sections = splitSections(body);
|
|
34656
|
+
const parsedVerification = extractVerification(sections);
|
|
34547
34657
|
const frontmatterVerification = data.verification && typeof data.verification === 'object'
|
|
34548
34658
|
? data.verification
|
|
34549
34659
|
: null;
|
|
34550
|
-
const verification =
|
|
34660
|
+
const verification = hasVerificationContent(parsedVerification)
|
|
34551
34661
|
? parsedVerification
|
|
34552
34662
|
: (frontmatterVerification || {});
|
|
34663
|
+
const version = Number(data.version);
|
|
34553
34664
|
return {
|
|
34554
34665
|
id: data.id || path_1.default.basename(filePath, '.md'),
|
|
34555
34666
|
status: data.status || 'draft',
|
|
34556
|
-
version:
|
|
34557
|
-
objective: data.objective || extractSection(
|
|
34667
|
+
version: Number.isFinite(version) && version >= 1 ? version : 1,
|
|
34668
|
+
objective: data.objective || extractSection(sections, 'Objective') || '',
|
|
34558
34669
|
title: data.title || data.userGoal || extractTitle(body) || 'Untitled Intent',
|
|
34559
34670
|
stageName: data.stage || undefined,
|
|
34560
34671
|
severity: data.severity || undefined,
|
|
34561
|
-
outcomes,
|
|
34562
|
-
|
|
34563
|
-
|
|
34564
|
-
|
|
34565
|
-
|
|
34672
|
+
outcomes: extractListSection(sections, 'Outcomes'),
|
|
34673
|
+
decisions: extractDecisions(sections),
|
|
34674
|
+
constraints: extractListSection(sections, 'Constraints'),
|
|
34675
|
+
edgeCases: extractEdgeCases(sections),
|
|
34676
|
+
healthMetrics: extractListSection(sections, 'Health Metrics'),
|
|
34677
|
+
scope: extractScope(sections) || undefined,
|
|
34566
34678
|
verification,
|
|
34567
34679
|
source: 'local',
|
|
34568
34680
|
};
|
|
@@ -34575,44 +34687,93 @@ function readIntentFile(filePath) {
|
|
|
34575
34687
|
// ============================================================
|
|
34576
34688
|
// Markdown Parsing Helpers
|
|
34577
34689
|
// ============================================================
|
|
34690
|
+
/**
|
|
34691
|
+
* Split the markdown body into `## Heading` → body lines. On a duplicate heading the last one
|
|
34692
|
+
* wins. `###` is not a section boundary (`^##\s+` cannot match `### `), so sub-headings stay
|
|
34693
|
+
* inside their parent section.
|
|
34694
|
+
*/
|
|
34695
|
+
function splitSections(body) {
|
|
34696
|
+
const sections = new Map();
|
|
34697
|
+
let current = null;
|
|
34698
|
+
for (const line of body.split('\n')) {
|
|
34699
|
+
const heading = line.match(/^##\s+(.+?)\s*$/);
|
|
34700
|
+
if (heading) {
|
|
34701
|
+
current = [];
|
|
34702
|
+
sections.set(heading[1], current);
|
|
34703
|
+
continue;
|
|
34704
|
+
}
|
|
34705
|
+
// An H1 ends the preceding section without opening a new one (it is the intent title).
|
|
34706
|
+
if (/^#\s+/.test(line)) {
|
|
34707
|
+
current = null;
|
|
34708
|
+
continue;
|
|
34709
|
+
}
|
|
34710
|
+
if (current)
|
|
34711
|
+
current.push(line);
|
|
34712
|
+
}
|
|
34713
|
+
return sections;
|
|
34714
|
+
}
|
|
34578
34715
|
function extractTitle(body) {
|
|
34579
34716
|
const match = body.match(/^#\s+(.+)$/m);
|
|
34580
34717
|
return match ? match[1].trim() : '';
|
|
34581
34718
|
}
|
|
34582
|
-
function
|
|
34583
|
-
|
|
34584
|
-
const match = body.match(regex);
|
|
34585
|
-
return match ? match[1].trim() : '';
|
|
34719
|
+
function sectionLines(sections, heading) {
|
|
34720
|
+
return sections.get(heading) ?? [];
|
|
34586
34721
|
}
|
|
34587
|
-
function
|
|
34588
|
-
|
|
34589
|
-
|
|
34590
|
-
|
|
34591
|
-
|
|
34592
|
-
|
|
34593
|
-
|
|
34594
|
-
|
|
34722
|
+
function extractSection(sections, heading) {
|
|
34723
|
+
return sectionLines(sections, heading).join('\n').trim();
|
|
34724
|
+
}
|
|
34725
|
+
/** Strip a list marker and an optional `[ ]` / `[x]` checkbox from a line. */
|
|
34726
|
+
function stripListMarker(line) {
|
|
34727
|
+
return line.replace(/^\s*[-*]\s+(\[.\]\s+)?/, '').trim();
|
|
34728
|
+
}
|
|
34729
|
+
function isListItem(line) {
|
|
34730
|
+
return /^\s*[-*]\s/.test(line);
|
|
34731
|
+
}
|
|
34732
|
+
function extractListSection(sections, heading) {
|
|
34733
|
+
return sectionLines(sections, heading)
|
|
34734
|
+
.filter(isListItem)
|
|
34735
|
+
.map(stripListMarker)
|
|
34595
34736
|
.filter(Boolean);
|
|
34596
34737
|
}
|
|
34597
|
-
|
|
34598
|
-
|
|
34599
|
-
|
|
34600
|
-
|
|
34738
|
+
/**
|
|
34739
|
+
* Parse `## Decisions & Ruled-Out Alternatives` entries written by `decisionLines()`:
|
|
34740
|
+
* - **choice** (instead of: ruledOut) — reason
|
|
34741
|
+
* The separator is an em dash when written by us; a plain hyphen is accepted for hand-edited files.
|
|
34742
|
+
*/
|
|
34743
|
+
function extractDecisions(sections) {
|
|
34744
|
+
const out = [];
|
|
34745
|
+
for (const line of sectionLines(sections, DECISIONS_HEADING)) {
|
|
34746
|
+
if (!isListItem(line))
|
|
34747
|
+
continue;
|
|
34748
|
+
const clean = stripListMarker(line);
|
|
34749
|
+
const match = clean.match(/^\*\*(.+?)\*\*\s*(?:\(instead of:\s*([^)]*)\)\s*)?[—–-]\s*(.+)$/);
|
|
34750
|
+
if (!match)
|
|
34751
|
+
continue;
|
|
34752
|
+
const choice = match[1].trim();
|
|
34753
|
+
const ruledOut = match[2]?.trim();
|
|
34754
|
+
const reason = match[3].trim();
|
|
34755
|
+
if (!choice || !reason)
|
|
34756
|
+
continue;
|
|
34757
|
+
out.push(ruledOut ? { choice, ruledOut, reason } : { choice, reason });
|
|
34758
|
+
}
|
|
34759
|
+
return out;
|
|
34760
|
+
}
|
|
34761
|
+
function extractEdgeCases(sections) {
|
|
34601
34762
|
const cases = [];
|
|
34602
|
-
const
|
|
34603
|
-
|
|
34604
|
-
|
|
34763
|
+
for (const line of sectionLines(sections, 'Edge Cases')) {
|
|
34764
|
+
if (!isListItem(line))
|
|
34765
|
+
continue;
|
|
34766
|
+
const clean = stripListMarker(line);
|
|
34605
34767
|
// Pattern: **scenario**: expected behavior
|
|
34606
34768
|
const match = clean.match(/^\*\*(.+?)\*\*:\s*(.+)$/);
|
|
34607
34769
|
if (match) {
|
|
34608
34770
|
cases.push({ scenario: match[1], expectedBehavior: match[2] });
|
|
34771
|
+
continue;
|
|
34609
34772
|
}
|
|
34610
|
-
|
|
34611
|
-
|
|
34612
|
-
|
|
34613
|
-
|
|
34614
|
-
cases.push({ scenario: arrowMatch[1].trim(), expectedBehavior: arrowMatch[2].trim() });
|
|
34615
|
-
}
|
|
34773
|
+
// Pattern: scenario → expected behavior
|
|
34774
|
+
const arrowMatch = clean.match(/^(.+?)\s*[→:]\s*(.+)$/);
|
|
34775
|
+
if (arrowMatch) {
|
|
34776
|
+
cases.push({ scenario: arrowMatch[1].trim(), expectedBehavior: arrowMatch[2].trim() });
|
|
34616
34777
|
}
|
|
34617
34778
|
}
|
|
34618
34779
|
return cases;
|
|
@@ -34621,24 +34782,27 @@ function extractEdgeCases(body) {
|
|
|
34621
34782
|
* Extract Scope section with **In scope:** and **Out of scope:** sub-lists.
|
|
34622
34783
|
* Matches the format emitted by formatIntentMd().
|
|
34623
34784
|
*/
|
|
34624
|
-
function extractScope(
|
|
34625
|
-
const
|
|
34626
|
-
if (
|
|
34785
|
+
function extractScope(sections) {
|
|
34786
|
+
const lines = sectionLines(sections, 'Scope');
|
|
34787
|
+
if (lines.length === 0)
|
|
34627
34788
|
return null;
|
|
34628
34789
|
const inScope = [];
|
|
34629
34790
|
const outOfScope = [];
|
|
34630
34791
|
let target = null;
|
|
34631
|
-
for (const line of
|
|
34632
|
-
|
|
34792
|
+
for (const line of lines) {
|
|
34793
|
+
const trimmed = line.trim();
|
|
34794
|
+
if (/^\*\*In scope[:*]*/i.test(trimmed)) {
|
|
34633
34795
|
target = inScope;
|
|
34634
34796
|
continue;
|
|
34635
34797
|
}
|
|
34636
|
-
if (/^\*\*Out of scope[
|
|
34798
|
+
if (/^\*\*Out of scope[:*]*/i.test(trimmed)) {
|
|
34637
34799
|
target = outOfScope;
|
|
34638
34800
|
continue;
|
|
34639
34801
|
}
|
|
34640
|
-
if (target && line
|
|
34641
|
-
|
|
34802
|
+
if (target && isListItem(line)) {
|
|
34803
|
+
const item = stripListMarker(line);
|
|
34804
|
+
if (item)
|
|
34805
|
+
target.push(item);
|
|
34642
34806
|
}
|
|
34643
34807
|
}
|
|
34644
34808
|
if (inScope.length === 0 && outOfScope.length === 0)
|
|
@@ -34650,39 +34814,214 @@ function extractScope(body) {
|
|
|
34650
34814
|
result.outOfScope = outOfScope;
|
|
34651
34815
|
return result;
|
|
34652
34816
|
}
|
|
34817
|
+
/** Canonical kind labels written by `VERIFICATION_KIND_LABELS` in intent-compiler.ts. */
|
|
34818
|
+
const VERIFICATION_LABEL_TO_KIND = {
|
|
34819
|
+
'fastest check': 'fastest',
|
|
34820
|
+
'shipped signal': 'shipped-signal',
|
|
34821
|
+
'regression guard': 'regression-guard',
|
|
34822
|
+
'manual check': 'manual',
|
|
34823
|
+
'automated test': 'test',
|
|
34824
|
+
};
|
|
34825
|
+
/** Legacy bucket labels from older intent.md files. Checked after the canonical labels. */
|
|
34826
|
+
const LEGACY_VERIFICATION_LABELS = [
|
|
34827
|
+
[/^e2e tests?$/i, 'e2eTests'],
|
|
34828
|
+
[/^unit tests?$/i, 'unitTests'],
|
|
34829
|
+
[/^manual checks$/i, 'manualChecks'],
|
|
34830
|
+
];
|
|
34831
|
+
/** Inverse of `renderCheckLine()`: `description (verifies: X) [passing]`. */
|
|
34832
|
+
function parseCheckLine(text) {
|
|
34833
|
+
let rest = text.trim();
|
|
34834
|
+
let status;
|
|
34835
|
+
let verifies;
|
|
34836
|
+
const statusMatch = rest.match(/\s*\[(unknown|passing|failing)\]$/i);
|
|
34837
|
+
if (statusMatch) {
|
|
34838
|
+
status = statusMatch[1].toLowerCase();
|
|
34839
|
+
rest = rest.slice(0, statusMatch.index).trim();
|
|
34840
|
+
}
|
|
34841
|
+
const verifiesMatch = rest.match(/\s*\(verifies:\s*([^)]*)\)$/i);
|
|
34842
|
+
if (verifiesMatch) {
|
|
34843
|
+
const value = verifiesMatch[1].trim();
|
|
34844
|
+
if (value)
|
|
34845
|
+
verifies = value;
|
|
34846
|
+
rest = rest.slice(0, verifiesMatch.index).trim();
|
|
34847
|
+
}
|
|
34848
|
+
return { description: rest, status, verifies };
|
|
34849
|
+
}
|
|
34850
|
+
/** Read a `**Label**:` heading inside the Verification section. */
|
|
34851
|
+
function parseVerificationLabel(line) {
|
|
34852
|
+
const match = line.trim().match(/^\*\*(.+?)\*\*\s*:?\s*$/);
|
|
34853
|
+
return match ? match[1].trim() : null;
|
|
34854
|
+
}
|
|
34653
34855
|
/**
|
|
34654
|
-
* Extract Verification section
|
|
34655
|
-
*
|
|
34856
|
+
* Extract the Verification section. Canonical kind labels ("Fastest check", "Shipped signal",
|
|
34857
|
+
* "Regression guard", "Manual check", "Automated test") become `checks[]`; legacy labels
|
|
34858
|
+
* ("E2E Tests", "Unit Tests", "Manual Checks") keep their string-array buckets, which
|
|
34859
|
+
* `toVerificationChecks()` in intent-compiler.ts adapts on read.
|
|
34656
34860
|
*/
|
|
34657
|
-
function extractVerification(
|
|
34658
|
-
const
|
|
34659
|
-
if (
|
|
34861
|
+
function extractVerification(sections) {
|
|
34862
|
+
const lines = sectionLines(sections, 'Verification');
|
|
34863
|
+
if (lines.length === 0)
|
|
34660
34864
|
return {};
|
|
34661
34865
|
const result = {};
|
|
34662
|
-
let
|
|
34663
|
-
|
|
34664
|
-
|
|
34665
|
-
|
|
34666
|
-
|
|
34667
|
-
|
|
34866
|
+
let currentKind = null;
|
|
34867
|
+
let currentLegacy = null;
|
|
34868
|
+
for (const line of lines) {
|
|
34869
|
+
const label = parseVerificationLabel(line);
|
|
34870
|
+
if (label) {
|
|
34871
|
+
const kind = VERIFICATION_LABEL_TO_KIND[label.toLowerCase()];
|
|
34872
|
+
if (kind) {
|
|
34873
|
+
currentKind = kind;
|
|
34874
|
+
currentLegacy = null;
|
|
34875
|
+
continue;
|
|
34876
|
+
}
|
|
34877
|
+
const legacy = LEGACY_VERIFICATION_LABELS.find(([re]) => re.test(label));
|
|
34878
|
+
if (legacy) {
|
|
34879
|
+
currentKind = null;
|
|
34880
|
+
currentLegacy = legacy[1];
|
|
34881
|
+
if (!result[currentLegacy])
|
|
34882
|
+
result[currentLegacy] = [];
|
|
34883
|
+
continue;
|
|
34884
|
+
}
|
|
34885
|
+
// Unknown bold label — stop attributing lines to the previous bucket.
|
|
34886
|
+
currentKind = null;
|
|
34887
|
+
currentLegacy = null;
|
|
34668
34888
|
continue;
|
|
34669
34889
|
}
|
|
34670
|
-
if (
|
|
34671
|
-
currentKey = 'unitTests';
|
|
34672
|
-
result[currentKey] = [];
|
|
34890
|
+
if (!isListItem(line))
|
|
34673
34891
|
continue;
|
|
34674
|
-
|
|
34675
|
-
if (
|
|
34676
|
-
currentKey = 'manualChecks';
|
|
34677
|
-
result[currentKey] = [];
|
|
34892
|
+
const text = stripListMarker(line);
|
|
34893
|
+
if (!text)
|
|
34678
34894
|
continue;
|
|
34895
|
+
if (currentKind) {
|
|
34896
|
+
const { description, status, verifies } = parseCheckLine(text);
|
|
34897
|
+
if (!description)
|
|
34898
|
+
continue;
|
|
34899
|
+
const check = { kind: currentKind, description };
|
|
34900
|
+
if (status)
|
|
34901
|
+
check.status = status;
|
|
34902
|
+
if (verifies)
|
|
34903
|
+
check.verifies = verifies;
|
|
34904
|
+
(result.checks ||= []).push(check);
|
|
34679
34905
|
}
|
|
34680
|
-
if (
|
|
34681
|
-
result[
|
|
34906
|
+
else if (currentLegacy) {
|
|
34907
|
+
result[currentLegacy].push(text);
|
|
34682
34908
|
}
|
|
34683
34909
|
}
|
|
34684
34910
|
return result;
|
|
34685
34911
|
}
|
|
34912
|
+
function hasVerificationContent(v) {
|
|
34913
|
+
return !!(v.checks?.length || v.e2eTests?.length || v.unitTests?.length || v.manualChecks?.length);
|
|
34914
|
+
}
|
|
34915
|
+
|
|
34916
|
+
|
|
34917
|
+
/***/ }),
|
|
34918
|
+
|
|
34919
|
+
/***/ 4681:
|
|
34920
|
+
/***/ ((__unused_webpack_module, exports) => {
|
|
34921
|
+
|
|
34922
|
+
"use strict";
|
|
34923
|
+
|
|
34924
|
+
/**
|
|
34925
|
+
* Idempotent merge of a Pathmode-generated section into an existing agent-instructions file
|
|
34926
|
+
* (CLAUDE.md / AGENTS.md). The section is wrapped in <!-- PATHMODE:START ... --> / <!-- PATHMODE:END -->
|
|
34927
|
+
* markers (both the local formatClaudeMdSection and the cloud generateClaudeMdContent emit them),
|
|
34928
|
+
* so re-running replaces the marked block in place instead of appending a duplicate. This is the
|
|
34929
|
+
* no-drift guarantee of the round-trip: the repo's Pathmode context is regenerated from the
|
|
34930
|
+
* canonical source, never hand-maintained.
|
|
34931
|
+
*/
|
|
34932
|
+
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
|
34933
|
+
exports.mergePathmodeSection = mergePathmodeSection;
|
|
34934
|
+
/** Matches the whole PATHMODE block, tolerant of the suffixed start marker
|
|
34935
|
+
* (`<!-- PATHMODE:START - Do not edit... -->`). Not global — there is one block per file. */
|
|
34936
|
+
const PATHMODE_SECTION_RE = /<!-- PATHMODE:START[\s\S]*?-->[\s\S]*?<!-- PATHMODE:END -->/;
|
|
34937
|
+
/** The section embeds a volatile `_Generated at <timestamp>_` line. Strip it so a re-generated
|
|
34938
|
+
* section that differs ONLY by a fresher timestamp still compares equal (reports 'unchanged' →
|
|
34939
|
+
* caller skips the write → no noisy timestamp-only diff that would mask real drift). */
|
|
34940
|
+
const GENERATED_AT_RE = /^_Generated at .*$/m;
|
|
34941
|
+
const withoutTimestamp = (s) => s.replace(GENERATED_AT_RE, '');
|
|
34942
|
+
/**
|
|
34943
|
+
* Merge `section` (a marker-wrapped Pathmode block) into `existing` file content.
|
|
34944
|
+
* - empty file -> the section becomes the whole file ('created')
|
|
34945
|
+
* - has a PATHMODE block, byte-identical to `section` -> no change ('unchanged' — proven no drift)
|
|
34946
|
+
* - has a PATHMODE block, different -> replace it in place ('replaced')
|
|
34947
|
+
* - no PATHMODE block -> append it after the existing content ('appended')
|
|
34948
|
+
* Pure; callers decide whether to actually write (skip the write when 'unchanged').
|
|
34949
|
+
*/
|
|
34950
|
+
function mergePathmodeSection(existing, section) {
|
|
34951
|
+
if (!existing)
|
|
34952
|
+
return { content: section, action: 'created' };
|
|
34953
|
+
const match = existing.match(PATHMODE_SECTION_RE);
|
|
34954
|
+
if (match) {
|
|
34955
|
+
if (withoutTimestamp(match[0]) === withoutTimestamp(section)) {
|
|
34956
|
+
return { content: existing, action: 'unchanged' };
|
|
34957
|
+
}
|
|
34958
|
+
// Replacer FUNCTION, not a string: `section` may contain `$&`, `$$`, `` $` ``, `$'`, `$1`
|
|
34959
|
+
// (a user's spec text), which String.replace would otherwise interpret as special patterns
|
|
34960
|
+
// and corrupt the merged file. The function form substitutes `section` verbatim.
|
|
34961
|
+
return { content: existing.replace(PATHMODE_SECTION_RE, () => section), action: 'replaced' };
|
|
34962
|
+
}
|
|
34963
|
+
// No existing block: append, separated by a blank line, normalizing trailing whitespace so
|
|
34964
|
+
// re-runs stay stable (the next run finds the marker and replaces in place).
|
|
34965
|
+
return { content: `${existing.replace(/\s*$/, '')}\n\n${section}\n`, action: 'appended' };
|
|
34966
|
+
}
|
|
34967
|
+
|
|
34968
|
+
|
|
34969
|
+
/***/ }),
|
|
34970
|
+
|
|
34971
|
+
/***/ 6137:
|
|
34972
|
+
/***/ ((__unused_webpack_module, exports) => {
|
|
34973
|
+
|
|
34974
|
+
"use strict";
|
|
34975
|
+
|
|
34976
|
+
/**
|
|
34977
|
+
* Save policy for intent.md.
|
|
34978
|
+
*
|
|
34979
|
+
* Extracted from the `intent_save` handler so the decision is testable: the handler itself lives
|
|
34980
|
+
* inside startMcpServer() and cannot be imported. The rules exist because the original handler
|
|
34981
|
+
* stamped a fresh `intent_${Date.now()}` id on every save and wrote unconditionally — a second
|
|
34982
|
+
* save in one conversation destroyed the first spec and reset its version and status.
|
|
34983
|
+
*/
|
|
34984
|
+
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
|
34985
|
+
exports.unquoteId = unquoteId;
|
|
34986
|
+
exports.decideSave = decideSave;
|
|
34987
|
+
/** Strip one layer of matching surrounding quotes, as picked up from raw YAML frontmatter text. */
|
|
34988
|
+
function unquoteId(value) {
|
|
34989
|
+
const trimmed = (value || '').trim();
|
|
34990
|
+
const m = trimmed.match(/^(['"])([\s\S]*)\1$/);
|
|
34991
|
+
return m ? m[2].trim() : trimmed;
|
|
34992
|
+
}
|
|
34993
|
+
/**
|
|
34994
|
+
* Decide how a save against an existing intent.md should be applied.
|
|
34995
|
+
*
|
|
34996
|
+
* - No existing file → create at version 1, status 'draft'.
|
|
34997
|
+
* - Existing file, same intent → update: bump version, preserve status and created.
|
|
34998
|
+
* "Same intent" means a matching id, OR no incoming id at all: the common conversational flow is
|
|
34999
|
+
* save → refine → save, where the agent holds a spec object it never gave an id.
|
|
35000
|
+
* - Existing file, different id → refuse, unless overwrite was explicitly requested. Refusing is
|
|
35001
|
+
* the safe default because the alternative silently destroys an unrelated spec.
|
|
35002
|
+
*
|
|
35003
|
+
* The incoming id is unquoted first: frontmatter serializes `id: "intent_123"`, and an agent that
|
|
35004
|
+
* reads intent.md as text rather than parsed YAML hands those quotes straight back. Comparing raw
|
|
35005
|
+
* would refuse a legitimate update and print two identical-looking ids in the error.
|
|
35006
|
+
*/
|
|
35007
|
+
function decideSave(params) {
|
|
35008
|
+
const { existing, overwrite, mintId } = params;
|
|
35009
|
+
const incomingId = unquoteId(params.incomingId);
|
|
35010
|
+
const isSameIntent = !!existing && (!incomingId || incomingId === existing.id);
|
|
35011
|
+
if (existing && !isSameIntent && !overwrite) {
|
|
35012
|
+
return { action: 'refuse', id: incomingId, version: existing.version, status: existing.status };
|
|
35013
|
+
}
|
|
35014
|
+
// An explicit overwrite of a different intent is a create: the previous spec's version and
|
|
35015
|
+
// status belonged to a different intent and must not carry over onto this one.
|
|
35016
|
+
const previous = isSameIntent ? existing : null;
|
|
35017
|
+
return {
|
|
35018
|
+
action: previous ? 'update' : 'create',
|
|
35019
|
+
id: incomingId || previous?.id || mintId(),
|
|
35020
|
+
version: previous ? previous.version + 1 : 1,
|
|
35021
|
+
status: previous ? previous.status : 'draft',
|
|
35022
|
+
created: previous?.created,
|
|
35023
|
+
};
|
|
35024
|
+
}
|
|
34686
35025
|
|
|
34687
35026
|
|
|
34688
35027
|
/***/ }),
|
|
@@ -34759,7 +35098,9 @@ function success(msg) { console.log(` ${GREEN}✓${RESET} ${msg}`); }
|
|
|
34759
35098
|
function warn(msg) { console.log(` ${YELLOW}!${RESET} ${msg}`); }
|
|
34760
35099
|
function fail(msg) { console.log(` ${RED}✗${RESET} ${msg}`); }
|
|
34761
35100
|
function getMcpServerBlock(apiKey, omitApiKey) {
|
|
34762
|
-
|
|
35101
|
+
// No key, or a config that may be committed to a repo: emit a bare block. The server infers
|
|
35102
|
+
// local mode when it finds no key, so this is a complete keyless setup, not a partial one.
|
|
35103
|
+
if (!apiKey || omitApiKey) {
|
|
34763
35104
|
return {
|
|
34764
35105
|
command: 'npx',
|
|
34765
35106
|
args: ['@pathmode/mcp-server'],
|
|
@@ -34819,57 +35160,62 @@ async function runSetup() {
|
|
|
34819
35160
|
log(`${BOLD}Pathmode MCP Setup${RESET}`);
|
|
34820
35161
|
log(`${DIM}──────────────────${RESET}`);
|
|
34821
35162
|
log('');
|
|
34822
|
-
|
|
34823
|
-
|
|
34824
|
-
|
|
34825
|
-
|
|
34826
|
-
|
|
34827
|
-
process.exit(1);
|
|
34828
|
-
}
|
|
34829
|
-
// ─── Step 1: Validate key ─────────────────────────────────
|
|
34830
|
-
process.stdout.write(` Validating API key...`);
|
|
35163
|
+
// ─── Step 1: Validate key (cloud mode) ────────────────────
|
|
35164
|
+
//
|
|
35165
|
+
// No key is NOT an error. The skill pack's documented first command is `setup`, and this used
|
|
35166
|
+
// to exit(1) on every keyless user — the first command in the onboarding path was a wall.
|
|
35167
|
+
// Keyless setup configures local mode, which is a complete, working configuration.
|
|
34831
35168
|
let workspaceName = '';
|
|
34832
35169
|
let workspaceId = '';
|
|
34833
35170
|
const apiUrl = args.includes('--staging')
|
|
34834
35171
|
? 'https://staging.pathmode.io'
|
|
34835
35172
|
: 'https://pathmode.io';
|
|
34836
|
-
|
|
34837
|
-
|
|
34838
|
-
|
|
34839
|
-
|
|
34840
|
-
'Content-Type': 'application/json',
|
|
34841
|
-
},
|
|
34842
|
-
});
|
|
34843
|
-
if (!res.ok) {
|
|
34844
|
-
if (res.status === 401 || res.status === 403) {
|
|
34845
|
-
log(` ${RED}✗${RESET}`);
|
|
34846
|
-
log('');
|
|
34847
|
-
fail('Invalid or expired API key.');
|
|
34848
|
-
log(` Get a new key from ${CYAN}${apiUrl}${RESET} → Settings → API Keys`);
|
|
34849
|
-
log('');
|
|
34850
|
-
process.exit(1);
|
|
34851
|
-
}
|
|
34852
|
-
throw new Error(`HTTP ${res.status}`);
|
|
34853
|
-
}
|
|
34854
|
-
const workspace = await res.json();
|
|
34855
|
-
workspaceName = workspace.name;
|
|
34856
|
-
workspaceId = workspace.id;
|
|
34857
|
-
log(` ${GREEN}✓${RESET}`);
|
|
34858
|
-
success(`Connected to "${BOLD}${workspaceName}${RESET}"`);
|
|
34859
|
-
}
|
|
34860
|
-
catch (err) {
|
|
34861
|
-
log(` ${RED}✗${RESET}`);
|
|
35173
|
+
if (!apiKey) {
|
|
35174
|
+
success(`Configuring ${BOLD}local mode${RESET} (no API key given)`);
|
|
35175
|
+
log(` ${DIM}Specs are written to intent.md in your project. Nothing leaves your machine.${RESET}`);
|
|
35176
|
+
log(` ${DIM}To connect a workspace later: npx @pathmode/mcp-server setup pm_live_xxx${RESET}`);
|
|
34862
35177
|
log('');
|
|
34863
|
-
|
|
34864
|
-
|
|
35178
|
+
}
|
|
35179
|
+
else {
|
|
35180
|
+
process.stdout.write(` Validating API key...`);
|
|
35181
|
+
try {
|
|
35182
|
+
const res = await fetch(`${apiUrl}/api/v1/workspace`, {
|
|
35183
|
+
headers: {
|
|
35184
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
35185
|
+
'Content-Type': 'application/json',
|
|
35186
|
+
},
|
|
35187
|
+
});
|
|
35188
|
+
if (!res.ok) {
|
|
35189
|
+
if (res.status === 401 || res.status === 403) {
|
|
35190
|
+
log(` ${RED}✗${RESET}`);
|
|
35191
|
+
log('');
|
|
35192
|
+
fail('Invalid or expired API key.');
|
|
35193
|
+
log(` Get a new key from ${CYAN}${apiUrl}${RESET} → Settings → API Keys`);
|
|
35194
|
+
log('');
|
|
35195
|
+
process.exit(1);
|
|
35196
|
+
}
|
|
35197
|
+
throw new Error(`HTTP ${res.status}`);
|
|
35198
|
+
}
|
|
35199
|
+
const workspace = await res.json();
|
|
35200
|
+
workspaceName = workspace.name;
|
|
35201
|
+
workspaceId = workspace.id;
|
|
35202
|
+
log(` ${GREEN}✓${RESET}`);
|
|
35203
|
+
success(`Connected to "${BOLD}${workspaceName}${RESET}"`);
|
|
34865
35204
|
}
|
|
34866
|
-
|
|
34867
|
-
|
|
35205
|
+
catch (err) {
|
|
35206
|
+
log(` ${RED}✗${RESET}`);
|
|
35207
|
+
log('');
|
|
35208
|
+
if (err.cause?.code === 'ENOTFOUND' || err.cause?.code === 'ECONNREFUSED') {
|
|
35209
|
+
fail('Could not reach pathmode.io. Check your internet connection.');
|
|
35210
|
+
}
|
|
35211
|
+
else {
|
|
35212
|
+
fail(`Connection failed: ${err.message}`);
|
|
35213
|
+
}
|
|
35214
|
+
log('');
|
|
35215
|
+
process.exit(1);
|
|
34868
35216
|
}
|
|
34869
35217
|
log('');
|
|
34870
|
-
process.exit(1);
|
|
34871
35218
|
}
|
|
34872
|
-
log('');
|
|
34873
35219
|
// ─── Step 2: Detect & configure tools ─────────────────────
|
|
34874
35220
|
let configured = 0;
|
|
34875
35221
|
for (const tool of TOOLS) {
|
|
@@ -34912,15 +35258,20 @@ async function runSetup() {
|
|
|
34912
35258
|
}
|
|
34913
35259
|
}
|
|
34914
35260
|
// ─── Step 3: Save ~/.pathmode/config.json ─────────────────
|
|
34915
|
-
|
|
34916
|
-
|
|
34917
|
-
|
|
34918
|
-
|
|
34919
|
-
|
|
34920
|
-
|
|
34921
|
-
|
|
34922
|
-
|
|
34923
|
-
|
|
35261
|
+
//
|
|
35262
|
+
// Only when we have a key. A keyless run must not touch this file: writing a keyless config
|
|
35263
|
+
// would silently disconnect a workspace the user set up earlier.
|
|
35264
|
+
if (apiKey) {
|
|
35265
|
+
const pathmodeConfigDir = path_1.default.join(os_1.default.homedir(), '.pathmode');
|
|
35266
|
+
const pathmodeConfigFile = path_1.default.join(pathmodeConfigDir, 'config.json');
|
|
35267
|
+
const pathmodeConfig = {
|
|
35268
|
+
apiKey,
|
|
35269
|
+
apiUrl,
|
|
35270
|
+
workspaceId,
|
|
35271
|
+
};
|
|
35272
|
+
if (writeJsonSafe(pathmodeConfigFile, pathmodeConfig)) {
|
|
35273
|
+
success(`Config saved → ${DIM}${shortenPath(pathmodeConfigFile)}${RESET}`);
|
|
35274
|
+
}
|
|
34924
35275
|
}
|
|
34925
35276
|
log('');
|
|
34926
35277
|
// ─── Step 4: Summary ──────────────────────────────────────
|
|
@@ -34932,8 +35283,10 @@ async function runSetup() {
|
|
|
34932
35283
|
log(` ${CYAN} "mcpServers": {${RESET}`);
|
|
34933
35284
|
log(` ${CYAN} "pathmode": {${RESET}`);
|
|
34934
35285
|
log(` ${CYAN} "command": "npx",${RESET}`);
|
|
34935
|
-
log(` ${CYAN} "args": ["@pathmode/mcp-server"]
|
|
34936
|
-
|
|
35286
|
+
log(` ${CYAN} "args": ["@pathmode/mcp-server"]${apiKey ? ',' : ''}${RESET}`);
|
|
35287
|
+
if (apiKey) {
|
|
35288
|
+
log(` ${CYAN} "env": { "PATHMODE_API_KEY": "${apiKey}" }${RESET}`);
|
|
35289
|
+
}
|
|
34937
35290
|
log(` ${CYAN} }${RESET}`);
|
|
34938
35291
|
log(` ${CYAN} }${RESET}`);
|
|
34939
35292
|
log(` ${CYAN}}${RESET}`);
|
|
@@ -34943,6 +35296,23 @@ async function runSetup() {
|
|
|
34943
35296
|
log(`${GREEN}Done!${RESET} Restart your tools to activate Pathmode.`);
|
|
34944
35297
|
log('');
|
|
34945
35298
|
}
|
|
35299
|
+
// ─── Step 5: The exact next step ──────────────────────────
|
|
35300
|
+
//
|
|
35301
|
+
// Setup that ends at "Done!" leaves the user to guess how to reach the product. Name the
|
|
35302
|
+
// phrase, because the skills auto-trigger on plain English rather than slash commands.
|
|
35303
|
+
log(`${BOLD}Next${RESET}`);
|
|
35304
|
+
log(` 1. Install the skill pack: ${CYAN}npx @pathmode/mcp-server install-skills${RESET}`);
|
|
35305
|
+
log(` 2. Restart Claude Code, then say:`);
|
|
35306
|
+
log('');
|
|
35307
|
+
log(` ${CYAN}"Help me write an intent spec for [the thing you're about to build]"${RESET}`);
|
|
35308
|
+
log('');
|
|
35309
|
+
if (apiKey) {
|
|
35310
|
+
log(` ${DIM}Specs sync to "${workspaceName}" and are visible to your team and other agents.${RESET}`);
|
|
35311
|
+
}
|
|
35312
|
+
else {
|
|
35313
|
+
log(` ${DIM}The spec is written to intent.md in your project root.${RESET}`);
|
|
35314
|
+
}
|
|
35315
|
+
log('');
|
|
34946
35316
|
}
|
|
34947
35317
|
|
|
34948
35318
|
|
|
@@ -64524,7 +64894,7 @@ module.exports = /*#__PURE__*/JSON.parse('{"$schema":"http://json-schema.org/dra
|
|
|
64524
64894
|
/***/ ((module) => {
|
|
64525
64895
|
|
|
64526
64896
|
"use strict";
|
|
64527
|
-
module.exports = /*#__PURE__*/JSON.parse('{"name":"@pathmode/mcp-server","version":"1.
|
|
64897
|
+
module.exports = /*#__PURE__*/JSON.parse('{"name":"@pathmode/mcp-server","version":"1.9.0","publishConfig":{"access":"public"},"mcpName":"io.github.pathmodeio/mcp-server","description":"Pathmode MCP Server — Build structured intent specs through Socratic AI conversation (zero-config), or connect to your Intent Layer for strategic context, dependency graphs, and implementation prompts.","main":"dist/index.js","bin":{"pathmode-mcp":"dist/index.js"},"files":["dist/","manifest.json","icon.svg","README.md","skills/"],"scripts":{"build":"rm -rf dist && ncc build src/index.ts -o dist","dev":"ts-node src/index.ts","prepublishOnly":"npm run build"},"keywords":["pathmode","mcp","model-context-protocol","claude-code","claude-code-skills","agent-skills","cursor","windsurf","intent-engineering","intent-compiler","ai-agents","product-development","dependency-graph","strategic-planning"],"author":"Pathmode","license":"MIT","type":"commonjs","engines":{"node":">=18.0.0"},"repository":{"type":"git","url":"git+https://github.com/pathmodeio/mcp-server.git","directory":"packages/mcp-server"},"homepage":"https://pathmode.io","dependencies":{"@modelcontextprotocol/sdk":"^1.12.1","gray-matter":"^4.0.3","zod":"^3.24.0"},"devDependencies":{"@types/node":"^25.1.0","@vercel/ncc":"^0.38.4","ts-node":"^10.9.2","typescript":"^5.9.3"}}');
|
|
64528
64898
|
|
|
64529
64899
|
/***/ })
|
|
64530
64900
|
|
|
@@ -64577,14 +64947,16 @@ var exports = __webpack_exports__;
|
|
|
64577
64947
|
* Connects Claude Code, Cursor, and other AI agents to your Intent Layer.
|
|
64578
64948
|
*
|
|
64579
64949
|
* Usage:
|
|
64580
|
-
* npx @pathmode/mcp-server # Cloud mode
|
|
64581
|
-
* npx @pathmode/mcp-server --local #
|
|
64582
|
-
* npx @pathmode/mcp-server setup
|
|
64950
|
+
* npx @pathmode/mcp-server # Cloud mode with a key, local mode without one
|
|
64951
|
+
* npx @pathmode/mcp-server --local # Force local mode (reads intent.md from cwd)
|
|
64952
|
+
* npx @pathmode/mcp-server setup # Configure your tools for keyless local mode
|
|
64953
|
+
* npx @pathmode/mcp-server setup pm_live_xxx # Configure your tools for cloud mode
|
|
64583
64954
|
* npx @pathmode/mcp-server install-skills # Copy the skill pack into .claude/skills/
|
|
64584
64955
|
* npx @pathmode/mcp-server install-skills --global # Install into ~/.claude/skills/ instead
|
|
64585
64956
|
*
|
|
64586
|
-
*
|
|
64587
|
-
*
|
|
64957
|
+
* Mode is inferred: with no API key configured (PATHMODE_API_KEY or ~/.pathmode/config.json)
|
|
64958
|
+
* the server runs in local mode and reads/writes intent.md in the project root. The Intent
|
|
64959
|
+
* Compiler (compile-intent prompt, intent_save, intent_export tools) needs no API key.
|
|
64588
64960
|
*
|
|
64589
64961
|
* Add to .mcp.json in the project root:
|
|
64590
64962
|
* {
|
|
@@ -64618,7 +64990,9 @@ const path_1 = __nccwpck_require__(6928);
|
|
|
64618
64990
|
const fs_1 = __nccwpck_require__(9896);
|
|
64619
64991
|
const api_client_1 = __nccwpck_require__(7475);
|
|
64620
64992
|
const local_reader_1 = __nccwpck_require__(3518);
|
|
64993
|
+
const save_policy_1 = __nccwpck_require__(6137);
|
|
64621
64994
|
const intent_compiler_1 = __nccwpck_require__(6488);
|
|
64995
|
+
const pathmode_section_1 = __nccwpck_require__(4681);
|
|
64622
64996
|
const setup_1 = __nccwpck_require__(8294);
|
|
64623
64997
|
const install_skills_1 = __nccwpck_require__(3783);
|
|
64624
64998
|
// Server version is sourced from package.json so the version reported to MCP
|
|
@@ -64646,18 +65020,21 @@ else {
|
|
|
64646
65020
|
}
|
|
64647
65021
|
function startMcpServer() {
|
|
64648
65022
|
// ─── MCP Server ───────────────────────────────────────────────
|
|
64649
|
-
const isLocalMode = process.argv.includes('--local');
|
|
64650
|
-
let client = null;
|
|
64651
65023
|
const isDebug = process.env.PATHMODE_MCP_DEBUG === '1';
|
|
64652
|
-
|
|
64653
|
-
|
|
64654
|
-
|
|
64655
|
-
|
|
64656
|
-
|
|
64657
|
-
|
|
64658
|
-
|
|
64659
|
-
|
|
64660
|
-
|
|
65024
|
+
// Local mode is the DEFAULT when no API key is configured. `--local` forces it even when a key
|
|
65025
|
+
// exists. Previously local mode required the flag, so the keyless install documented on
|
|
65026
|
+
// pathmode.io (no key, no flag) fell through to the cloud path with a null client: every tool
|
|
65027
|
+
// failed, including the four that can read intent.md. Keyless users could write a spec and never
|
|
65028
|
+
// read it back.
|
|
65029
|
+
const cloudConfig = (0, api_client_1.loadConfig)();
|
|
65030
|
+
const hasApiKey = !!cloudConfig?.apiKey;
|
|
65031
|
+
const isLocalMode = process.argv.includes('--local') || !hasApiKey;
|
|
65032
|
+
let client = null;
|
|
65033
|
+
if (isDebug) {
|
|
65034
|
+
console.error(`[pathmode-mcp] mode: ${isLocalMode ? 'local' : 'cloud'}, API key present: ${hasApiKey}, url: ${cloudConfig?.apiUrl || 'none'}`);
|
|
65035
|
+
}
|
|
65036
|
+
if (!isLocalMode && cloudConfig) {
|
|
65037
|
+
client = new api_client_1.PathmodeClient(cloudConfig);
|
|
64661
65038
|
}
|
|
64662
65039
|
// ============================================================
|
|
64663
65040
|
// Server Setup
|
|
@@ -64679,9 +65056,28 @@ function startMcpServer() {
|
|
|
64679
65056
|
class CloudClientError extends Error {
|
|
64680
65057
|
constructor() { super(CLOUD_REQUIRED_MSG); this.name = 'CloudClientError'; }
|
|
64681
65058
|
}
|
|
65059
|
+
const cloudOnly = isLocalMode
|
|
65060
|
+
? { registerTool: (() => undefined) }
|
|
65061
|
+
: server;
|
|
64682
65062
|
function normalizeText(value) {
|
|
64683
65063
|
return (value || '').trim();
|
|
64684
65064
|
}
|
|
65065
|
+
// Clamp a caller-supplied write path to the project root (process.cwd()).
|
|
65066
|
+
// The optional `path` argument on intent_save / intent_export is influenced by
|
|
65067
|
+
// the MCP client, so an absolute path or `..` segments could otherwise land a
|
|
65068
|
+
// file outside the intended project folder. Reject those before any fs write.
|
|
65069
|
+
function resolveWithinProject(requestedPath) {
|
|
65070
|
+
const root = process.cwd();
|
|
65071
|
+
if ((0, path_1.isAbsolute)(requestedPath)) {
|
|
65072
|
+
throw new Error(`Refusing to write outside the project: "${requestedPath}" is an absolute path. Pass a path relative to the project root.`);
|
|
65073
|
+
}
|
|
65074
|
+
const resolved = (0, path_1.resolve)(root, requestedPath);
|
|
65075
|
+
const rel = (0, path_1.relative)(root, resolved);
|
|
65076
|
+
if (rel === '' || rel.startsWith('..') || (0, path_1.isAbsolute)(rel)) {
|
|
65077
|
+
throw new Error(`Refusing to write outside the project: "${requestedPath}" resolves outside the project root.`);
|
|
65078
|
+
}
|
|
65079
|
+
return resolved;
|
|
65080
|
+
}
|
|
64685
65081
|
// Keep this selection heuristic aligned with the canonical readiness rules in
|
|
64686
65082
|
// /Users/jannelammi/code/Pathmode/lib/intentReadiness.ts. The MCP package cannot
|
|
64687
65083
|
// import app/lib code directly, so this mirrors only the minimum logic needed
|
|
@@ -64701,6 +65097,24 @@ function startMcpServer() {
|
|
|
64701
65097
|
return anyReady;
|
|
64702
65098
|
return intents[0];
|
|
64703
65099
|
}
|
|
65100
|
+
/** The union of evidence IDs an intent cites: directly linked + section anchors + decision-cited.
|
|
65101
|
+
* Mirrors lib/intentSpecHelpers.collectSpecEvidenceIds (the MCP package can't import app/lib). */
|
|
65102
|
+
function collectCitedEvidenceIds(intent) {
|
|
65103
|
+
const anchorIds = Object.values((intent?.evidenceAnchors || {})).flat();
|
|
65104
|
+
const decisionIds = (intent?.decisions || []).flatMap((d) => d?.evidenceIds || []);
|
|
65105
|
+
const all = [...(intent?.evidenceIds || []), ...anchorIds, ...decisionIds];
|
|
65106
|
+
return Array.from(new Set(all.filter((id) => typeof id === 'string' && id.length > 0)));
|
|
65107
|
+
}
|
|
65108
|
+
/** Cheap, fetch-free coverage summary from the intent's own fields. Tells an agent how much
|
|
65109
|
+
* evidence backs the intent and where to get the full text, without bloating the default call. */
|
|
65110
|
+
function summarizeCitedEvidence(intent) {
|
|
65111
|
+
return {
|
|
65112
|
+
citedCount: collectCitedEvidenceIds(intent).length,
|
|
65113
|
+
linkedCount: (intent?.evidenceIds || []).length,
|
|
65114
|
+
anchoredSections: Object.keys(intent?.evidenceAnchors || {}).length,
|
|
65115
|
+
note: 'Evidence is referenced by ID. Call get_agent_prompt for the full evidence-backed execution prompt, or re-call get_current_intent with include_evidence=true to inline the evidence content.',
|
|
65116
|
+
};
|
|
65117
|
+
}
|
|
64704
65118
|
// Note: The MCP SDK catches errors thrown in tool handlers and returns them as
|
|
64705
65119
|
// error text results. CloudClientError thrown by requireCloudClient() will
|
|
64706
65120
|
// surface its message to the client without crashing the server.
|
|
@@ -64709,10 +65123,13 @@ function startMcpServer() {
|
|
|
64709
65123
|
// ============================================================
|
|
64710
65124
|
server.registerTool('get_current_intent', {
|
|
64711
65125
|
title: 'Get Current Intent',
|
|
64712
|
-
description: 'Get the currently active intent, preferring approved intents with real objective/outcome content over empty stubs. Returns the full IntentSpec
|
|
64713
|
-
inputSchema: {
|
|
65126
|
+
description: 'Get the currently active intent, preferring approved intents with real objective/outcome content over empty stubs. Returns the full IntentSpec plus an evidenceSummary (how much user evidence backs it). Evidence is referenced by ID; pass include_evidence=true to inline the evidence content, or call get_agent_prompt for the full evidence-backed execution prompt.',
|
|
65127
|
+
inputSchema: {
|
|
65128
|
+
status: zod_1.z.string().optional().describe('Filter by status: draft, validated, approved, shipped, verified'),
|
|
65129
|
+
include_evidence: zod_1.z.boolean().optional().describe('Inline the actual evidence items (truncated) this intent cites. Default false — the default response carries only a fetch-free evidenceSummary to keep casual calls cheap.'),
|
|
65130
|
+
},
|
|
64714
65131
|
annotations: READ_ONLY,
|
|
64715
|
-
}, async ({ status }) => {
|
|
65132
|
+
}, async ({ status, include_evidence }) => {
|
|
64716
65133
|
if (isLocalMode) {
|
|
64717
65134
|
const intents = (0, local_reader_1.readLocalIntents)();
|
|
64718
65135
|
const filtered = status ? intents.filter(i => i.status === status) : intents;
|
|
@@ -64720,18 +65137,47 @@ function startMcpServer() {
|
|
|
64720
65137
|
if (!current) {
|
|
64721
65138
|
return { content: [{ type: 'text', text: 'No intents found locally.' }] };
|
|
64722
65139
|
}
|
|
65140
|
+
// Local mode has no cloud evidence store — return the intent as-is.
|
|
64723
65141
|
return { content: [{ type: 'text', text: JSON.stringify(current, null, 2) }] };
|
|
64724
65142
|
}
|
|
64725
65143
|
const cloud = requireCloudClient();
|
|
64726
|
-
|
|
65144
|
+
let intents = await cloud.listIntents(status || 'approved');
|
|
64727
65145
|
if (intents.length === 0) {
|
|
64728
|
-
|
|
64729
|
-
if (
|
|
65146
|
+
intents = await cloud.listIntents();
|
|
65147
|
+
if (intents.length === 0) {
|
|
64730
65148
|
return { content: [{ type: 'text', text: 'No intents found in workspace.' }] };
|
|
64731
65149
|
}
|
|
64732
|
-
return { content: [{ type: 'text', text: JSON.stringify(pickCurrentIntent(allIntents), null, 2) }] };
|
|
64733
65150
|
}
|
|
64734
|
-
|
|
65151
|
+
const current = pickCurrentIntent(intents);
|
|
65152
|
+
// Default: a fetch-free coverage summary (no extra API call). On request: the cited
|
|
65153
|
+
// evidence content, fetched once for the product and filtered to this intent's citations.
|
|
65154
|
+
const evidenceSummary = summarizeCitedEvidence(current);
|
|
65155
|
+
let evidence;
|
|
65156
|
+
let unresolvedEvidenceIds;
|
|
65157
|
+
if (include_evidence && current?.productId) {
|
|
65158
|
+
const cited = collectCitedEvidenceIds(current);
|
|
65159
|
+
if (cited.length > 0) {
|
|
65160
|
+
// Pull up to the API max (200) and filter to this intent's citations. A cited item
|
|
65161
|
+
// older than that page won't resolve — surface it as unresolvedEvidenceIds rather
|
|
65162
|
+
// than silently returning an incomplete evidence block.
|
|
65163
|
+
const { evidence: pool } = await cloud.queryEvidence({ productId: current.productId, limit: 200 });
|
|
65164
|
+
const found = new Map(pool.map(e => [e.id, e]));
|
|
65165
|
+
evidence = cited
|
|
65166
|
+
.map(id => found.get(id))
|
|
65167
|
+
.filter((e) => !!e)
|
|
65168
|
+
.map(e => ({
|
|
65169
|
+
id: e.id,
|
|
65170
|
+
type: e.type,
|
|
65171
|
+
content: e.content.length > 200 ? `${e.content.slice(0, 200)}…` : e.content,
|
|
65172
|
+
source: e.source,
|
|
65173
|
+
severity: e.severity,
|
|
65174
|
+
}));
|
|
65175
|
+
const missing = cited.filter(id => !found.has(id));
|
|
65176
|
+
if (missing.length > 0)
|
|
65177
|
+
unresolvedEvidenceIds = missing;
|
|
65178
|
+
}
|
|
65179
|
+
}
|
|
65180
|
+
return { content: [{ type: 'text', text: JSON.stringify({ ...current, evidenceSummary, ...(evidence ? { evidence } : {}), ...(unresolvedEvidenceIds ? { unresolvedEvidenceIds } : {}) }, null, 2) }] };
|
|
64735
65181
|
});
|
|
64736
65182
|
server.registerTool('list_intents', {
|
|
64737
65183
|
title: 'List Intents',
|
|
@@ -64760,7 +65206,7 @@ function startMcpServer() {
|
|
|
64760
65206
|
});
|
|
64761
65207
|
server.registerTool('get_intent', {
|
|
64762
65208
|
title: 'Get Intent',
|
|
64763
|
-
description: 'Get a single intent by ID with full details including objective, outcomes, constraints, edge cases, and relations.',
|
|
65209
|
+
description: 'Get a single intent by ID with full details including objective, outcomes, constraints, edge cases, and relations. Evidence is referenced by ID; call get_agent_prompt for the full evidence-backed execution prompt.',
|
|
64764
65210
|
inputSchema: { intentId: zod_1.z.string().describe('The intent ID to fetch') },
|
|
64765
65211
|
annotations: READ_ONLY,
|
|
64766
65212
|
}, async ({ intentId }) => {
|
|
@@ -64780,7 +65226,7 @@ function startMcpServer() {
|
|
|
64780
65226
|
return { content: [{ type: 'text', text: `Failed to fetch intent: ${e.message}` }] };
|
|
64781
65227
|
}
|
|
64782
65228
|
});
|
|
64783
|
-
|
|
65229
|
+
cloudOnly.registerTool('get_intent_relations', {
|
|
64784
65230
|
title: 'Get Intent Relations',
|
|
64785
65231
|
description: 'Get the dependency graph for a specific intent. Shows what it depends on, enables, or blocks.',
|
|
64786
65232
|
inputSchema: { intentId: zod_1.z.string().describe('The intent ID to get relations for') },
|
|
@@ -64839,7 +65285,7 @@ function startMcpServer() {
|
|
|
64839
65285
|
return { content: [{ type: 'text', text: `Search failed: ${e.message}` }] };
|
|
64840
65286
|
}
|
|
64841
65287
|
});
|
|
64842
|
-
|
|
65288
|
+
cloudOnly.registerTool('analyze_intent_graph', {
|
|
64843
65289
|
title: 'Analyze Intent Graph',
|
|
64844
65290
|
description: 'Analyze the intent dependency graph for risks and strategic insights. Returns critical path, cycles, bottlenecks, orphans, status mismatches, and stalled intents.',
|
|
64845
65291
|
inputSchema: {
|
|
@@ -65029,27 +65475,29 @@ function startMcpServer() {
|
|
|
65029
65475
|
});
|
|
65030
65476
|
/** Map a cloud ApiIntent into the IntentFields shape the formatters consume. */
|
|
65031
65477
|
function apiIntentToFields(intent) {
|
|
65032
|
-
const v = (intent.verification || {});
|
|
65033
65478
|
return {
|
|
65034
65479
|
id: intent.id,
|
|
65035
65480
|
title: intent.title,
|
|
65036
65481
|
objective: intent.objective,
|
|
65037
65482
|
outcomes: intent.outcomes ?? [],
|
|
65483
|
+
decisions: intent.decisions,
|
|
65038
65484
|
constraints: intent.constraints,
|
|
65039
65485
|
edgeCases: (intent.edgeCases ?? []).map((e) => ({ scenario: e.scenario, expectedBehavior: e.expectedBehavior })),
|
|
65040
65486
|
healthMetrics: intent.healthMetrics,
|
|
65041
65487
|
scope: intent.scope,
|
|
65042
|
-
verification
|
|
65488
|
+
// Pass the whole verification through (checks[] + legacy buckets) so feedback-loop checks
|
|
65489
|
+
// survive the cloud→local bridge; the formatters adapt both shapes uniformly.
|
|
65490
|
+
verification: (intent.verification || undefined),
|
|
65043
65491
|
implementationContext: intent.implementationContext ?? undefined,
|
|
65044
65492
|
};
|
|
65045
65493
|
}
|
|
65046
|
-
|
|
65494
|
+
cloudOnly.registerTool('export_context', {
|
|
65047
65495
|
title: 'Export Context',
|
|
65048
|
-
description: 'Export workspace context as a formatted file. Use "claude-md" for CLAUDE.md (full workspace context), "cursorrules" for Cursor AI rules, "intent-md" for a single intent specification file, or "outcome-rubric" for a Claude Managed Agents Outcomes rubric (writer description + evidence-forcing grader rubric) derived from the resolved intent, its implementation context, and the workspace constitution. For cursorrules/intent-md/outcome-rubric, product context is always derived from the resolved intent. For claude-md, pass productId to select a specific product, otherwise the first active product is used.',
|
|
65496
|
+
description: 'Export workspace context as a formatted file. Use "claude-md" for CLAUDE.md (full workspace context), "agents-md" for the same workspace context written for AGENTS.md (the native instruction file for OpenAI Codex and modern Cursor), "cursorrules" for Cursor AI rules, "intent-md" for a single intent specification file, or "outcome-rubric" for a Claude Managed Agents Outcomes rubric (writer description + evidence-forcing grader rubric) derived from the resolved intent, its implementation context, and the workspace constitution. claude-md and agents-md produce identical, agent-agnostic content (the difference is only the target filename). For cursorrules/intent-md/outcome-rubric, product context is always derived from the resolved intent. For claude-md/agents-md, pass productId to select a specific product, otherwise the first active product is used.',
|
|
65049
65497
|
inputSchema: {
|
|
65050
|
-
format: zod_1.z.enum(['claude-md', 'cursorrules', 'intent-md', 'outcome-rubric']).describe('Export format'),
|
|
65498
|
+
format: zod_1.z.enum(['claude-md', 'agents-md', 'cursorrules', 'intent-md', 'outcome-rubric']).describe('Export format'),
|
|
65051
65499
|
intentId: zod_1.z.string().optional().describe('Intent ID (optional, for cursorrules and intent-md)'),
|
|
65052
|
-
productId: zod_1.z.string().optional().describe('Product ID (optional, only used for claude-md format to select a specific product)'),
|
|
65500
|
+
productId: zod_1.z.string().optional().describe('Product ID (optional, only used for claude-md/agents-md format to select a specific product)'),
|
|
65053
65501
|
},
|
|
65054
65502
|
annotations: READ_ONLY,
|
|
65055
65503
|
}, async ({ format, intentId, productId }) => {
|
|
@@ -65083,7 +65531,42 @@ function startMcpServer() {
|
|
|
65083
65531
|
return { content: [{ type: 'text', text: `Export failed: ${e.message}` }] };
|
|
65084
65532
|
}
|
|
65085
65533
|
});
|
|
65086
|
-
server.
|
|
65534
|
+
server.tool('sync_context', 'Write this workspace\'s canonical Pathmode context into the repo\'s CLAUDE.md (or AGENTS.md), idempotently — the round-trip. Pulls the latest from Pathmode and replaces the PATHMODE-marked section in place, so the repo\'s agent instructions never drift from the source of truth. Unlike export_context (which returns the text for you to read), this writes the file; re-running when nothing changed reports "no drift". Run it after the canonical context changes in Pathmode.', {
|
|
65535
|
+
format: zod_1.z.enum(['claude-md', 'agents-md']).optional().describe('Target file: claude-md → CLAUDE.md (default), agents-md → AGENTS.md (Codex and other AGENTS.md-aware agents).'),
|
|
65536
|
+
productId: zod_1.z.string().optional().describe('Product ID (optional; defaults to the first active product).'),
|
|
65537
|
+
path: zod_1.z.string().optional().describe('Output file path relative to the project root. Must stay inside the project (absolute paths and ".." are rejected). Defaults to CLAUDE.md / AGENTS.md.'),
|
|
65538
|
+
}, async ({ format, productId, path }) => {
|
|
65539
|
+
if (isLocalMode) {
|
|
65540
|
+
return { content: [{ type: 'text', text: 'Sync requires cloud mode. Use PATHMODE_API_KEY to connect.' }] };
|
|
65541
|
+
}
|
|
65542
|
+
const fmt = format || 'claude-md';
|
|
65543
|
+
try {
|
|
65544
|
+
// Pull the canonical, marker-wrapped section from the cloud (generateClaudeMdContent emits
|
|
65545
|
+
// the same <!-- PATHMODE:START/END --> markers the merge keys on).
|
|
65546
|
+
const section = await requireCloudClient().exportContext(fmt, undefined, productId);
|
|
65547
|
+
const defaultFile = fmt === 'agents-md' ? 'AGENTS.md' : 'CLAUDE.md';
|
|
65548
|
+
const filePath = resolveWithinProject(path || defaultFile);
|
|
65549
|
+
let existing = '';
|
|
65550
|
+
try {
|
|
65551
|
+
existing = (0, fs_1.readFileSync)(filePath, 'utf-8');
|
|
65552
|
+
}
|
|
65553
|
+
catch { /* file doesn't exist yet */ }
|
|
65554
|
+
const merged = (0, pathmode_section_1.mergePathmodeSection)(existing, section);
|
|
65555
|
+
if (merged.action !== 'unchanged')
|
|
65556
|
+
(0, fs_1.writeFileSync)(filePath, merged.content, 'utf-8');
|
|
65557
|
+
const msg = {
|
|
65558
|
+
unchanged: `✓ ${filePath} is already in sync with Pathmode — no drift.`,
|
|
65559
|
+
created: `✓ Created ${filePath} from your Pathmode context.`,
|
|
65560
|
+
appended: `✓ Added the Pathmode context section to ${filePath}.`,
|
|
65561
|
+
replaced: `✓ Synced ${filePath} — refreshed the Pathmode context section from canonical.`,
|
|
65562
|
+
};
|
|
65563
|
+
return { content: [{ type: 'text', text: msg[merged.action] }] };
|
|
65564
|
+
}
|
|
65565
|
+
catch (e) {
|
|
65566
|
+
return { content: [{ type: 'text', text: `Sync failed: ${e.message}` }] };
|
|
65567
|
+
}
|
|
65568
|
+
});
|
|
65569
|
+
cloudOnly.registerTool('get_agent_prompt', {
|
|
65087
65570
|
title: 'Get Agent Prompt',
|
|
65088
65571
|
description: 'Get a formatted execution prompt for a specific intent. This is the full structured prompt including objective, outcomes, constraints, edge cases, and verification steps.',
|
|
65089
65572
|
inputSchema: {
|
|
@@ -65103,7 +65586,7 @@ function startMcpServer() {
|
|
|
65103
65586
|
}]
|
|
65104
65587
|
};
|
|
65105
65588
|
});
|
|
65106
|
-
|
|
65589
|
+
cloudOnly.registerTool('get_workspace', {
|
|
65107
65590
|
title: 'Get Workspace',
|
|
65108
65591
|
description: 'Get workspace details including strategy (vision, non-negotiables, architecture principles), active products, and constitution rules.',
|
|
65109
65592
|
annotations: READ_ONLY,
|
|
@@ -65119,7 +65602,7 @@ function startMcpServer() {
|
|
|
65119
65602
|
}]
|
|
65120
65603
|
};
|
|
65121
65604
|
});
|
|
65122
|
-
|
|
65605
|
+
cloudOnly.registerTool('get_constitution', {
|
|
65123
65606
|
title: 'Get Constitution',
|
|
65124
65607
|
description: 'Get the workspace constitution rules. These are mandatory constraints that all implementations must respect.',
|
|
65125
65608
|
annotations: READ_ONLY,
|
|
@@ -65138,7 +65621,7 @@ function startMcpServer() {
|
|
|
65138
65621
|
// ============================================================
|
|
65139
65622
|
// Tools — Write Operations
|
|
65140
65623
|
// ============================================================
|
|
65141
|
-
|
|
65624
|
+
cloudOnly.registerTool('update_intent_status', {
|
|
65142
65625
|
title: 'Update Intent Status',
|
|
65143
65626
|
description: 'Update the status of an intent. Use this to mark an intent as shipped after implementation, or verified after testing. For shipped/verified transitions, the response includes a verification checklist of outcomes, constitution rules, and health metrics that should be confirmed.',
|
|
65144
65627
|
inputSchema: {
|
|
@@ -65167,7 +65650,7 @@ function startMcpServer() {
|
|
|
65167
65650
|
}]
|
|
65168
65651
|
};
|
|
65169
65652
|
});
|
|
65170
|
-
|
|
65653
|
+
cloudOnly.registerTool('log_implementation_note', {
|
|
65171
65654
|
title: 'Log Implementation Note',
|
|
65172
65655
|
description: 'Record a technical decision or implementation note for an intent. Use this to document why you chose a specific approach.',
|
|
65173
65656
|
inputSchema: {
|
|
@@ -65187,7 +65670,32 @@ function startMcpServer() {
|
|
|
65187
65670
|
}]
|
|
65188
65671
|
};
|
|
65189
65672
|
});
|
|
65190
|
-
|
|
65673
|
+
cloudOnly.registerTool('record_implementation_finding', {
|
|
65674
|
+
title: 'Record Implementation Finding',
|
|
65675
|
+
description: 'Write a correction back to an intent when BUILDING it revealed the spec was wrong. Use this the moment you discover the spec assumed something the implementation contradicts — instead of only fixing it in your head or this chat, record it so the NEXT agent or person inherits the correction. This is the inverse of a decision: it closes the refinement loop so the same stale premise is not rebuilt. Open findings ride into future agent prompts until a human reconciles them into the spec.',
|
|
65676
|
+
inputSchema: {
|
|
65677
|
+
intentId: zod_1.z.string().describe('The intent ID whose spec the finding is about'),
|
|
65678
|
+
assumption: zod_1.z.string().describe('What the spec assumed or said before you built it'),
|
|
65679
|
+
finding: zod_1.z.string().describe('What building it actually revealed — the fact that contradicts the assumption'),
|
|
65680
|
+
target: zod_1.z.string().optional().describe('Which part of the spec this contradicts: "objective", "outcome:<id>", "constraint:<index>", "edgeCase:<id>", "check:<id>" (a verification check — recording this flips that check to failing), or plain prose'),
|
|
65681
|
+
correction: zod_1.z.string().optional().describe('Your proposed correction to the intent, if you have one'),
|
|
65682
|
+
source: zod_1.z.string().optional().describe('Where this came from, e.g. "claude-code @ owner/repo" — provenance for the audit trail'),
|
|
65683
|
+
},
|
|
65684
|
+
annotations: WRITE_OP,
|
|
65685
|
+
}, async ({ intentId, assumption, finding, target, correction, source }) => {
|
|
65686
|
+
if (isLocalMode) {
|
|
65687
|
+
return { content: [{ type: 'text', text: 'Recording findings requires cloud mode. Use PATHMODE_API_KEY to connect.' }] };
|
|
65688
|
+
}
|
|
65689
|
+
const result = await requireCloudClient().recordFinding(intentId, { assumption, finding, target, correction, source });
|
|
65690
|
+
const openCount = result?.openCount;
|
|
65691
|
+
return {
|
|
65692
|
+
content: [{
|
|
65693
|
+
type: 'text',
|
|
65694
|
+
text: `Finding recorded for intent ${intentId}. The next agent that pulls this intent will see it flagged as unreconciled until a human folds it into the spec.${typeof openCount === 'number' ? ` Open findings now: ${openCount}.` : ''}`
|
|
65695
|
+
}]
|
|
65696
|
+
};
|
|
65697
|
+
});
|
|
65698
|
+
cloudOnly.registerTool('create_intent', {
|
|
65191
65699
|
title: 'Create Intent',
|
|
65192
65700
|
description: 'Create a new intent spec in the workspace. Requires at minimum a title, objective, and productId. Returns the created intent with its ID. Use list_intents first to see existing intents and avoid duplicates.',
|
|
65193
65701
|
inputSchema: {
|
|
@@ -65202,6 +65710,12 @@ function startMcpServer() {
|
|
|
65202
65710
|
expectedBehavior: zod_1.z.string(),
|
|
65203
65711
|
})).optional().describe('Failure modes and boundary conditions'),
|
|
65204
65712
|
verification: zod_1.z.object({
|
|
65713
|
+
checks: zod_1.z.array(zod_1.z.object({
|
|
65714
|
+
kind: zod_1.z.enum(['fastest', 'manual', 'shipped-signal', 'regression-guard', 'test']),
|
|
65715
|
+
description: zod_1.z.string(),
|
|
65716
|
+
status: zod_1.z.enum(['unknown', 'passing', 'failing']).optional(),
|
|
65717
|
+
verifies: zod_1.z.string().optional(),
|
|
65718
|
+
})).optional().describe('Feedback-loop checks (preferred over the legacy test buckets)'),
|
|
65205
65719
|
manualChecks: zod_1.z.array(zod_1.z.string()).optional(),
|
|
65206
65720
|
unitTests: zod_1.z.array(zod_1.z.string()).optional(),
|
|
65207
65721
|
e2eTests: zod_1.z.array(zod_1.z.string()).optional(),
|
|
@@ -65221,7 +65735,7 @@ function startMcpServer() {
|
|
|
65221
65735
|
}]
|
|
65222
65736
|
};
|
|
65223
65737
|
});
|
|
65224
|
-
|
|
65738
|
+
cloudOnly.registerTool('update_intent', {
|
|
65225
65739
|
title: 'Update Intent',
|
|
65226
65740
|
description: "Update an existing intent's content. Provide only the fields you want to change. Does NOT change intent status (use update_intent_status for that).",
|
|
65227
65741
|
inputSchema: {
|
|
@@ -65236,6 +65750,12 @@ function startMcpServer() {
|
|
|
65236
65750
|
expectedBehavior: zod_1.z.string(),
|
|
65237
65751
|
})).optional().describe('Replace all edge cases'),
|
|
65238
65752
|
verification: zod_1.z.object({
|
|
65753
|
+
checks: zod_1.z.array(zod_1.z.object({
|
|
65754
|
+
kind: zod_1.z.enum(['fastest', 'manual', 'shipped-signal', 'regression-guard', 'test']),
|
|
65755
|
+
description: zod_1.z.string(),
|
|
65756
|
+
status: zod_1.z.enum(['unknown', 'passing', 'failing']).optional(),
|
|
65757
|
+
verifies: zod_1.z.string().optional(),
|
|
65758
|
+
})).optional().describe('Feedback-loop checks (preferred over the legacy test buckets)'),
|
|
65239
65759
|
manualChecks: zod_1.z.array(zod_1.z.string()).optional(),
|
|
65240
65760
|
unitTests: zod_1.z.array(zod_1.z.string()).optional(),
|
|
65241
65761
|
e2eTests: zod_1.z.array(zod_1.z.string()).optional(),
|
|
@@ -65256,7 +65776,7 @@ function startMcpServer() {
|
|
|
65256
65776
|
}]
|
|
65257
65777
|
};
|
|
65258
65778
|
});
|
|
65259
|
-
|
|
65779
|
+
cloudOnly.registerTool('query_evidence', {
|
|
65260
65780
|
title: 'Query Evidence',
|
|
65261
65781
|
description: 'Search evidence items (friction points, user quotes, observations, metrics, feature requests) by product, type, severity, or text. Returns matching evidence with IDs that can be linked to intents.',
|
|
65262
65782
|
inputSchema: {
|
|
@@ -65280,7 +65800,7 @@ function startMcpServer() {
|
|
|
65280
65800
|
}]
|
|
65281
65801
|
};
|
|
65282
65802
|
});
|
|
65283
|
-
|
|
65803
|
+
cloudOnly.registerTool('create_evidence', {
|
|
65284
65804
|
title: 'Create Evidence',
|
|
65285
65805
|
description: 'Create a new evidence item (e.g., a discovered bug, user feedback quote, behavioral observation, or feature request). Evidence can later be linked to intents to support prioritization.',
|
|
65286
65806
|
inputSchema: {
|
|
@@ -65307,7 +65827,7 @@ function startMcpServer() {
|
|
|
65307
65827
|
}]
|
|
65308
65828
|
};
|
|
65309
65829
|
});
|
|
65310
|
-
|
|
65830
|
+
cloudOnly.registerTool('link_evidence', {
|
|
65311
65831
|
title: 'Link Evidence to Intent',
|
|
65312
65832
|
description: 'Link or unlink evidence items to/from an intent. Linking evidence to intents establishes traceability between user problems and planned solutions.',
|
|
65313
65833
|
inputSchema: {
|
|
@@ -65333,7 +65853,7 @@ function startMcpServer() {
|
|
|
65333
65853
|
}]
|
|
65334
65854
|
};
|
|
65335
65855
|
});
|
|
65336
|
-
|
|
65856
|
+
cloudOnly.registerTool('verify_implementation', {
|
|
65337
65857
|
title: 'Verify Implementation',
|
|
65338
65858
|
description: 'AI-grade your implementation against the intent spec. Checks each outcome, constraint, constitution rule, and edge case. Returns pass/fail per item with reasoning and an overall score. Also logs the result as an implementation note.',
|
|
65339
65859
|
inputSchema: {
|
|
@@ -65415,9 +65935,15 @@ function startMcpServer() {
|
|
|
65415
65935
|
// Intent Compiler — Zero-config tools (no API key needed)
|
|
65416
65936
|
// ============================================================
|
|
65417
65937
|
const intentSpecSchema = {
|
|
65938
|
+
id: zod_1.z.string().optional().describe('Existing intent id. Pass it back when re-saving an intent you already saved so the file is updated (version bumped, status preserved) instead of replaced. Omit for a new intent.'),
|
|
65418
65939
|
title: zod_1.z.string().describe('Short name for the intent'),
|
|
65419
65940
|
objective: zod_1.z.string().describe('Why this matters — the problem and who has it'),
|
|
65420
65941
|
outcomes: zod_1.z.array(zod_1.z.string()).describe('Observable, testable state changes'),
|
|
65942
|
+
decisions: zod_1.z.array(zod_1.z.object({
|
|
65943
|
+
choice: zod_1.z.string(),
|
|
65944
|
+
ruledOut: zod_1.z.string().optional(),
|
|
65945
|
+
reason: zod_1.z.string(),
|
|
65946
|
+
})).optional().describe('Decisions settled during design + the alternatives ruled out, so the agent does not relitigate them'),
|
|
65421
65947
|
constraints: zod_1.z.array(zod_1.z.string()).optional().describe('Hard limits the implementation must respect'),
|
|
65422
65948
|
edgeCases: zod_1.z.array(zod_1.z.object({
|
|
65423
65949
|
scenario: zod_1.z.string(),
|
|
@@ -65429,6 +65955,12 @@ function startMcpServer() {
|
|
|
65429
65955
|
outOfScope: zod_1.z.array(zod_1.z.string()).optional().describe('What is explicitly out of scope'),
|
|
65430
65956
|
}).optional().describe('Scope boundaries — what to build and what to avoid'),
|
|
65431
65957
|
verification: zod_1.z.object({
|
|
65958
|
+
checks: zod_1.z.array(zod_1.z.object({
|
|
65959
|
+
kind: zod_1.z.enum(['fastest', 'manual', 'shipped-signal', 'regression-guard', 'test']),
|
|
65960
|
+
description: zod_1.z.string(),
|
|
65961
|
+
status: zod_1.z.enum(['unknown', 'passing', 'failing']).optional(),
|
|
65962
|
+
verifies: zod_1.z.string().optional(),
|
|
65963
|
+
})).optional().describe('Feedback-loop checks (preferred over the legacy test buckets)'),
|
|
65432
65964
|
manualChecks: zod_1.z.array(zod_1.z.string()).optional(),
|
|
65433
65965
|
unitTests: zod_1.z.array(zod_1.z.string()).optional(),
|
|
65434
65966
|
e2eTests: zod_1.z.array(zod_1.z.string()).optional(),
|
|
@@ -65445,28 +65977,52 @@ function startMcpServer() {
|
|
|
65445
65977
|
}],
|
|
65446
65978
|
};
|
|
65447
65979
|
});
|
|
65448
|
-
server.tool('intent_save', 'Save an intent spec to intent.md in the project root. Called after building a spec through conversation.', {
|
|
65980
|
+
server.tool('intent_save', 'Save an intent spec to intent.md in the project root. Called after building a spec through conversation. Re-saving the same intent bumps its version and preserves its status; it never silently replaces a different intent.', {
|
|
65449
65981
|
spec: zod_1.z.object(intentSpecSchema),
|
|
65450
|
-
path: zod_1.z.string().optional().describe('File path relative to
|
|
65451
|
-
|
|
65452
|
-
|
|
65453
|
-
const
|
|
65982
|
+
path: zod_1.z.string().optional().describe('File path relative to the project root. Must stay inside the project (absolute paths and ".." are rejected). Defaults to intent.md'),
|
|
65983
|
+
overwrite: zod_1.z.boolean().optional().describe('Replace the file even when it already holds a DIFFERENT intent. Default false — the save is refused instead, so an unrelated spec is never clobbered.'),
|
|
65984
|
+
}, async ({ spec, path, overwrite }) => {
|
|
65985
|
+
const filePath = resolveWithinProject(path || 'intent.md');
|
|
65986
|
+
const existing = (0, local_reader_1.readIntentMeta)(filePath);
|
|
65987
|
+
const decision = (0, save_policy_1.decideSave)({
|
|
65988
|
+
existing,
|
|
65989
|
+
incomingId: spec.id,
|
|
65990
|
+
overwrite,
|
|
65991
|
+
mintId: () => `intent_${Date.now()}`,
|
|
65992
|
+
});
|
|
65993
|
+
if (decision.action === 'refuse') {
|
|
65994
|
+
return {
|
|
65995
|
+
content: [{
|
|
65996
|
+
type: 'text',
|
|
65997
|
+
text: [
|
|
65998
|
+
`✗ Refusing to overwrite ${filePath}`,
|
|
65999
|
+
'',
|
|
66000
|
+
`It already holds a different intent (id: ${existing.id ?? 'unknown'}, status: ${existing.status}, version: ${existing.version}), and this spec is "${decision.id}".`,
|
|
66001
|
+
'',
|
|
66002
|
+
'Save to another path (pass `path`), or pass overwrite=true to replace it deliberately.',
|
|
66003
|
+
].join('\n'),
|
|
66004
|
+
}],
|
|
66005
|
+
};
|
|
66006
|
+
}
|
|
66007
|
+
const { id, version, status, created } = decision;
|
|
66008
|
+
const content = (0, intent_compiler_1.formatIntentMd)({ ...spec, id }, { version, status, created });
|
|
65454
66009
|
(0, fs_1.writeFileSync)(filePath, content, 'utf-8');
|
|
66010
|
+
const action = decision.action === 'update' ? `Updated intent spec (v${version})` : 'Saved intent spec';
|
|
65455
66011
|
return {
|
|
65456
66012
|
content: [{
|
|
65457
66013
|
type: 'text',
|
|
65458
|
-
text: `✓
|
|
66014
|
+
text: `✓ ${action} at ${filePath}\n id: ${id} · status: ${status}\n\nTo connect this to Pathmode for dependency tracking and team collaboration, visit pathmode.io`,
|
|
65459
66015
|
}],
|
|
65460
66016
|
};
|
|
65461
66017
|
});
|
|
65462
66018
|
server.tool('intent_export', 'Export an intent spec as .cursorrules, a CLAUDE.md or AGENTS.md section, or a Claude Managed Agents Outcomes rubric for AI agent consumption. Use agents-md for Codex, Cursor, and other AGENTS.md-aware agents.', {
|
|
65463
66019
|
format: zod_1.z.enum(['cursorrules', 'claude-md', 'agents-md', 'outcome-rubric']).describe('Export format'),
|
|
65464
66020
|
spec: zod_1.z.object(intentSpecSchema),
|
|
65465
|
-
path: zod_1.z.string().optional().describe('Output file path. Defaults to .cursorrules, CLAUDE.md, or AGENTS.md'),
|
|
66021
|
+
path: zod_1.z.string().optional().describe('Output file path relative to the project root. Must stay inside the project (absolute paths and ".." are rejected). Defaults to .cursorrules, CLAUDE.md, or AGENTS.md'),
|
|
65466
66022
|
}, async ({ format, spec, path }) => {
|
|
65467
66023
|
if (format === 'cursorrules') {
|
|
65468
66024
|
const content = (0, intent_compiler_1.formatCursorRules)(spec);
|
|
65469
|
-
const filePath = (
|
|
66025
|
+
const filePath = resolveWithinProject(path || '.cursorrules');
|
|
65470
66026
|
(0, fs_1.writeFileSync)(filePath, content, 'utf-8');
|
|
65471
66027
|
return {
|
|
65472
66028
|
content: [{
|
|
@@ -65477,7 +66033,7 @@ function startMcpServer() {
|
|
|
65477
66033
|
}
|
|
65478
66034
|
else if (format === 'outcome-rubric') {
|
|
65479
66035
|
const content = (0, intent_compiler_1.formatOutcomeRubric)(spec);
|
|
65480
|
-
const filePath = (
|
|
66036
|
+
const filePath = resolveWithinProject(path || 'outcome-rubric.md');
|
|
65481
66037
|
(0, fs_1.writeFileSync)(filePath, content, 'utf-8');
|
|
65482
66038
|
return {
|
|
65483
66039
|
content: [{
|
|
@@ -65489,28 +66045,24 @@ function startMcpServer() {
|
|
|
65489
66045
|
else {
|
|
65490
66046
|
const section = (0, intent_compiler_1.formatClaudeMdSection)(spec);
|
|
65491
66047
|
const defaultFile = format === 'agents-md' ? 'AGENTS.md' : 'CLAUDE.md';
|
|
65492
|
-
const filePath = (
|
|
65493
|
-
// Append or replace PATHMODE section in existing file
|
|
66048
|
+
const filePath = resolveWithinProject(path || defaultFile);
|
|
65494
66049
|
let existing = '';
|
|
65495
66050
|
try {
|
|
65496
66051
|
existing = (0, fs_1.readFileSync)(filePath, 'utf-8');
|
|
65497
66052
|
}
|
|
65498
66053
|
catch { /* file doesn't exist yet */ }
|
|
65499
|
-
|
|
65500
|
-
|
|
65501
|
-
|
|
65502
|
-
const marker = /<!-- PATHMODE:START[\s\S]*?-->[\s\S]*?<!-- PATHMODE:END -->/;
|
|
65503
|
-
const updated = marker.test(existing)
|
|
65504
|
-
? existing.replace(marker, section)
|
|
65505
|
-
: existing ? existing + '\n\n' + section : section;
|
|
65506
|
-
(0, fs_1.writeFileSync)(filePath, updated, 'utf-8');
|
|
66054
|
+
const merged = (0, pathmode_section_1.mergePathmodeSection)(existing, section);
|
|
66055
|
+
if (merged.action !== 'unchanged')
|
|
66056
|
+
(0, fs_1.writeFileSync)(filePath, merged.content, 'utf-8');
|
|
65507
66057
|
const audience = format === 'agents-md'
|
|
65508
66058
|
? 'Codex, Cursor, and other AGENTS.md-aware agents'
|
|
65509
66059
|
: 'Claude Code';
|
|
65510
66060
|
return {
|
|
65511
66061
|
content: [{
|
|
65512
66062
|
type: 'text',
|
|
65513
|
-
text:
|
|
66063
|
+
text: merged.action === 'unchanged'
|
|
66064
|
+
? `✓ ${filePath} already has this intent's context — no change.`
|
|
66065
|
+
: `✓ Exported ${defaultFile} section to ${filePath}\n\n${audience} will now see this intent as context in every conversation.`,
|
|
65514
66066
|
}],
|
|
65515
66067
|
};
|
|
65516
66068
|
}
|
|
@@ -65626,6 +66178,22 @@ function startMcpServer() {
|
|
|
65626
66178
|
// ============================================================
|
|
65627
66179
|
async function main() {
|
|
65628
66180
|
const transport = new stdio_js_1.StdioServerTransport();
|
|
66181
|
+
// Tell Pathmode we launched. Hooked to oninitialized rather than placed after
|
|
66182
|
+
// connect() because the client's identity only exists once the MCP handshake has
|
|
66183
|
+
// completed — read it any earlier and getClientVersion() is undefined. Registered
|
|
66184
|
+
// before connect() so we cannot miss the callback. Fire-and-forget: this is the
|
|
66185
|
+
// only signal separating "installed but idle" from "never installed", but it must
|
|
66186
|
+
// never cost the user a working server. See PathmodeClient.handshake.
|
|
66187
|
+
server.server.oninitialized = () => {
|
|
66188
|
+
if (!client)
|
|
66189
|
+
return;
|
|
66190
|
+
const info = server.server.getClientVersion();
|
|
66191
|
+
void client.handshake({
|
|
66192
|
+
client: info?.name,
|
|
66193
|
+
clientVersion: info?.version,
|
|
66194
|
+
serverVersion: SERVER_VERSION,
|
|
66195
|
+
});
|
|
66196
|
+
};
|
|
65629
66197
|
await server.connect(transport);
|
|
65630
66198
|
}
|
|
65631
66199
|
main().catch((error) => {
|