@pathmode/mcp-server 1.13.1 → 1.14.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 +4 -2
- package/dist/api-client.d.ts +121 -1
- package/dist/api-client.d.ts.map +1 -1
- package/dist/index.js +641 -24
- package/dist/intent-compiler.d.ts +15 -8
- package/dist/intent-compiler.d.ts.map +1 -1
- package/dist/local-reader.d.ts +7 -0
- package/dist/local-reader.d.ts.map +1 -1
- package/dist/measurement-schema.d.ts +325 -0
- package/dist/measurement-schema.d.ts.map +1 -0
- package/dist/push-spec.d.ts +78 -0
- package/dist/push-spec.d.ts.map +1 -0
- package/manifest.json +1 -1
- package/package.json +1 -1
- package/skills/compile-intent/SKILL.md +5 -0
- package/skills/handoff-intent/SKILL.md +5 -1
- package/skills/preflight/SKILL.md +1 -1
package/dist/index.js
CHANGED
|
@@ -33543,13 +33543,24 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
33543
33543
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
33544
33544
|
};
|
|
33545
33545
|
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
|
33546
|
-
exports.PathmodeClient = void 0;
|
|
33546
|
+
exports.PathmodeClient = exports.ApiError = void 0;
|
|
33547
33547
|
exports.normalizeApiKey = normalizeApiKey;
|
|
33548
33548
|
exports.loadConfig = loadConfig;
|
|
33549
33549
|
const fs_1 = __importDefault(__nccwpck_require__(9896));
|
|
33550
33550
|
const path_1 = __importDefault(__nccwpck_require__(6928));
|
|
33551
33551
|
const os_1 = __importDefault(__nccwpck_require__(857));
|
|
33552
33552
|
const isDebug = process.env.PATHMODE_MCP_DEBUG === '1';
|
|
33553
|
+
/** An API failure that keeps the response body, so callers can act on `code` and its payload. */
|
|
33554
|
+
class ApiError extends Error {
|
|
33555
|
+
status;
|
|
33556
|
+
code;
|
|
33557
|
+
body;
|
|
33558
|
+
constructor(message) {
|
|
33559
|
+
super(message);
|
|
33560
|
+
this.name = 'ApiError';
|
|
33561
|
+
}
|
|
33562
|
+
}
|
|
33563
|
+
exports.ApiError = ApiError;
|
|
33553
33564
|
const CONFIG_DIR = path_1.default.join(os_1.default.homedir(), '.pathmode');
|
|
33554
33565
|
const CONFIG_FILE = path_1.default.join(CONFIG_DIR, 'config.json');
|
|
33555
33566
|
/**
|
|
@@ -33620,7 +33631,15 @@ class PathmodeClient {
|
|
|
33620
33631
|
'(1) re-run "npx @pathmode/mcp-server setup pm_live_xxx" with a fresh key from your workspace settings at https://pathmode.io, or ' +
|
|
33621
33632
|
'(2) remove PATHMODE_API_KEY (and ~/.pathmode/config.json) and restart to use free keyless local mode, where specs live in intent.md and no account is needed.');
|
|
33622
33633
|
}
|
|
33623
|
-
|
|
33634
|
+
// Carry the structured body, not just a sentence. PRODUCT_REQUIRED ships the candidate
|
|
33635
|
+
// list precisely so the agent can ask which product and retry; flattening it to a string
|
|
33636
|
+
// threw away the only thing that made the error actionable.
|
|
33637
|
+
const err = new ApiError(`API error (${response.status}): ${body.error || response.statusText}`);
|
|
33638
|
+
err.status = response.status;
|
|
33639
|
+
const structured = body;
|
|
33640
|
+
err.code = typeof structured.code === 'string' ? structured.code : undefined;
|
|
33641
|
+
err.body = body;
|
|
33642
|
+
throw err;
|
|
33624
33643
|
}
|
|
33625
33644
|
return response;
|
|
33626
33645
|
}
|
|
@@ -33657,6 +33676,45 @@ class PathmodeClient {
|
|
|
33657
33676
|
console.error(`[pathmode-mcp] handshake failed (ignored): ${e}`);
|
|
33658
33677
|
}
|
|
33659
33678
|
}
|
|
33679
|
+
/**
|
|
33680
|
+
* Report that one tool fired. Cloud mode only — see instrumentToolTelemetry in
|
|
33681
|
+
* index.ts, which is the thing that decides whether this is ever reached.
|
|
33682
|
+
*
|
|
33683
|
+
* The handshake tells the server a client launched; nothing told it whether the
|
|
33684
|
+
* client did anything. Three tools compute and write entirely on this machine and
|
|
33685
|
+
* reach no data endpoint, so with a valid key their invocations produced neither an
|
|
33686
|
+
* `api_call` nor any other trace: 140 launches and zero calls in 13.5 hours reads
|
|
33687
|
+
* the same whether the user hammered the preflight loop or never invoked a thing.
|
|
33688
|
+
*
|
|
33689
|
+
* Carries a tool name and an outcome. Never arguments, spec content, paths, or
|
|
33690
|
+
* anything read out of the user's repo — the server-side allowlist rejects a name
|
|
33691
|
+
* it does not recognise, so this stays a name-and-outcome channel even if a future
|
|
33692
|
+
* caller here gets careless.
|
|
33693
|
+
*
|
|
33694
|
+
* Silent by contract, exactly like handshake(): never throws, never writes to
|
|
33695
|
+
* stdout (stdout belongs to JSON-RPC), never awaited on the tool's path. A tool
|
|
33696
|
+
* result must not depend on our analytics reaching us. Deliberately does NOT go
|
|
33697
|
+
* through `fetch()` above, which logs to stderr and throws on non-2xx.
|
|
33698
|
+
*/
|
|
33699
|
+
async recordToolCall(tool, outcome) {
|
|
33700
|
+
try {
|
|
33701
|
+
const res = await fetch(`${this.apiUrl}/api/v1/connection/tool-call`, {
|
|
33702
|
+
method: 'POST',
|
|
33703
|
+
headers: {
|
|
33704
|
+
'Authorization': `Bearer ${this.apiKey}`,
|
|
33705
|
+
'Content-Type': 'application/json',
|
|
33706
|
+
},
|
|
33707
|
+
body: JSON.stringify({ tool, outcome }),
|
|
33708
|
+
signal: AbortSignal.timeout(5000),
|
|
33709
|
+
});
|
|
33710
|
+
if (isDebug)
|
|
33711
|
+
console.error(`[pathmode-mcp] tool-call ${tool}/${outcome}: ${res.status}`);
|
|
33712
|
+
}
|
|
33713
|
+
catch (e) {
|
|
33714
|
+
if (isDebug)
|
|
33715
|
+
console.error(`[pathmode-mcp] tool-call failed (ignored): ${e}`);
|
|
33716
|
+
}
|
|
33717
|
+
}
|
|
33660
33718
|
async listIntents(status) {
|
|
33661
33719
|
const params = status ? `?status=${status}` : '';
|
|
33662
33720
|
const res = await this.fetch(`/intents${params}`);
|
|
@@ -33685,6 +33743,35 @@ class PathmodeClient {
|
|
|
33685
33743
|
});
|
|
33686
33744
|
return res.json();
|
|
33687
33745
|
}
|
|
33746
|
+
/**
|
|
33747
|
+
* Hand back what the repo looks like where an intent lands.
|
|
33748
|
+
*
|
|
33749
|
+
* Provenance is deliberately not a parameter: the server stamps source, timestamp and spec
|
|
33750
|
+
* version, and ignores any attempt to supply them. `expectedSpecVersion` is the one exception,
|
|
33751
|
+
* and it is a question rather than a claim — "was the spec still this when I read it?" — answered
|
|
33752
|
+
* with 409 if not.
|
|
33753
|
+
*/
|
|
33754
|
+
/**
|
|
33755
|
+
* Append one decision to an intent's audit trail.
|
|
33756
|
+
*
|
|
33757
|
+
* Its own endpoint rather than a field on create/update, because that route validates every
|
|
33758
|
+
* evidence id against the workspace and drops the ones that do not resolve — a caller must not be
|
|
33759
|
+
* able to fabricate provenance by writing the column directly.
|
|
33760
|
+
*/
|
|
33761
|
+
async recordDecision(intentId, input) {
|
|
33762
|
+
const res = await this.fetch(`/intents/${intentId}/decisions`, {
|
|
33763
|
+
method: 'POST',
|
|
33764
|
+
body: JSON.stringify(input),
|
|
33765
|
+
});
|
|
33766
|
+
return res.json();
|
|
33767
|
+
}
|
|
33768
|
+
async saveImplementationContext(intentId, input) {
|
|
33769
|
+
const res = await this.fetch(`/intents/${intentId}/implementation-context`, {
|
|
33770
|
+
method: 'POST',
|
|
33771
|
+
body: JSON.stringify(input),
|
|
33772
|
+
});
|
|
33773
|
+
return res.json();
|
|
33774
|
+
}
|
|
33688
33775
|
async recordFinding(intentId, input) {
|
|
33689
33776
|
const res = await this.fetch(`/intents/${intentId}/findings`, {
|
|
33690
33777
|
method: 'POST',
|
|
@@ -33692,6 +33779,13 @@ class PathmodeClient {
|
|
|
33692
33779
|
});
|
|
33693
33780
|
return res.json();
|
|
33694
33781
|
}
|
|
33782
|
+
async recordOutcomeMeasurement(intentId, input) {
|
|
33783
|
+
const res = await this.fetch(`/intents/${intentId}/outcomes`, {
|
|
33784
|
+
method: 'POST',
|
|
33785
|
+
body: JSON.stringify(input),
|
|
33786
|
+
});
|
|
33787
|
+
return res.json();
|
|
33788
|
+
}
|
|
33695
33789
|
async getWorkspace() {
|
|
33696
33790
|
const res = await this.fetch('/workspace');
|
|
33697
33791
|
return res.json();
|
|
@@ -34026,7 +34120,7 @@ async function runInstallSkills() {
|
|
|
34026
34120
|
/***/ }),
|
|
34027
34121
|
|
|
34028
34122
|
/***/ 6488:
|
|
34029
|
-
/***/ ((__unused_webpack_module, exports) => {
|
|
34123
|
+
/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => {
|
|
34030
34124
|
|
|
34031
34125
|
"use strict";
|
|
34032
34126
|
|
|
@@ -34046,6 +34140,9 @@ exports.formatIntentMd = formatIntentMd;
|
|
|
34046
34140
|
exports.formatCursorRules = formatCursorRules;
|
|
34047
34141
|
exports.formatClaudeMdSection = formatClaudeMdSection;
|
|
34048
34142
|
exports.formatOutcomeRubric = formatOutcomeRubric;
|
|
34143
|
+
// Verification check collection. Mirrors lib/verification in the main app — this package is
|
|
34144
|
+
// standalone (own ncc build) and can't import it, so the type + adapter live here. `kind` is the
|
|
34145
|
+
const crypto_1 = __nccwpck_require__(6982);
|
|
34049
34146
|
const VERIFICATION_KIND_LABELS = {
|
|
34050
34147
|
fastest: 'Fastest check',
|
|
34051
34148
|
'shipped-signal': 'Shipped signal',
|
|
@@ -34237,14 +34334,61 @@ function decisionLines(decisions, heading) {
|
|
|
34237
34334
|
* is saved twice must keep its identity and its lifecycle state. Only a genuinely new intent
|
|
34238
34335
|
* (no id) gets a minted id, version 1, and status 'draft'.
|
|
34239
34336
|
*/
|
|
34337
|
+
/**
|
|
34338
|
+
* The body of `## Implementation Context` in intent.md.
|
|
34339
|
+
*
|
|
34340
|
+
* Two sources, one section. Local mode carries prose the agent gathered; cloud intents carry the
|
|
34341
|
+
* analyzer's structured object. Rendering both under the same heading is what lets a spec move
|
|
34342
|
+
* between modes without the section disappearing on the way through.
|
|
34343
|
+
*/
|
|
34344
|
+
function implementationContextBody(spec) {
|
|
34345
|
+
const text = spec.implementationContextText?.trim();
|
|
34346
|
+
if (text)
|
|
34347
|
+
return [text];
|
|
34348
|
+
const ic = spec.implementationContext;
|
|
34349
|
+
if (!ic)
|
|
34350
|
+
return [];
|
|
34351
|
+
const lines = [];
|
|
34352
|
+
if (ic.relevantAreas?.length) {
|
|
34353
|
+
lines.push('### Relevant areas');
|
|
34354
|
+
for (const a of ic.relevantAreas) {
|
|
34355
|
+
if (a?.path?.trim())
|
|
34356
|
+
lines.push(`- \`${a.path}\`${a.reason?.trim() ? ` — ${a.reason}` : ''}`);
|
|
34357
|
+
}
|
|
34358
|
+
}
|
|
34359
|
+
if (ic.currentBehavior?.trim()) {
|
|
34360
|
+
if (lines.length)
|
|
34361
|
+
lines.push('');
|
|
34362
|
+
lines.push('### Current behavior');
|
|
34363
|
+
lines.push(ic.currentBehavior.trim());
|
|
34364
|
+
}
|
|
34365
|
+
if (ic.risks?.length) {
|
|
34366
|
+
if (lines.length)
|
|
34367
|
+
lines.push('');
|
|
34368
|
+
lines.push('### Risks');
|
|
34369
|
+
for (const r of ic.risks)
|
|
34370
|
+
if (r?.trim())
|
|
34371
|
+
lines.push(`- ${r.trim()}`);
|
|
34372
|
+
}
|
|
34373
|
+
if (ic.verificationSuggestions?.length) {
|
|
34374
|
+
if (lines.length)
|
|
34375
|
+
lines.push('');
|
|
34376
|
+
lines.push('### Verification suggestions');
|
|
34377
|
+
for (const v of ic.verificationSuggestions)
|
|
34378
|
+
if (v?.trim())
|
|
34379
|
+
lines.push(`- ${v.trim()}`);
|
|
34380
|
+
}
|
|
34381
|
+
return lines;
|
|
34382
|
+
}
|
|
34240
34383
|
function formatIntentMd(spec, opts = {}) {
|
|
34241
34384
|
const now = new Date().toISOString();
|
|
34242
34385
|
const frontmatter = {
|
|
34243
|
-
id: spec.id ||
|
|
34386
|
+
id: spec.id || (0, crypto_1.randomUUID)(),
|
|
34244
34387
|
version: opts.version && opts.version >= 1 ? opts.version : 1,
|
|
34245
34388
|
status: opts.status || 'draft',
|
|
34246
34389
|
...(opts.readiness ? { readiness: opts.readiness } : {}),
|
|
34247
34390
|
...(opts.source ? { source: opts.source } : {}),
|
|
34391
|
+
...(opts.specVersion ? { specVersion: opts.specVersion } : {}),
|
|
34248
34392
|
created: opts.created || now,
|
|
34249
34393
|
updated: now,
|
|
34250
34394
|
};
|
|
@@ -34269,6 +34413,15 @@ function formatIntentMd(spec, opts = {}) {
|
|
|
34269
34413
|
sections.push('## Current State');
|
|
34270
34414
|
sections.push(spec.currentState.trim());
|
|
34271
34415
|
}
|
|
34416
|
+
// Sits next to Current State on purpose: one is what the author says is true today, the other is
|
|
34417
|
+
// what the repo says. Same heading the cloud agent prompt emits, so a handoff reads identically
|
|
34418
|
+
// in either mode.
|
|
34419
|
+
const icBody = implementationContextBody(spec);
|
|
34420
|
+
if (icBody.length) {
|
|
34421
|
+
sections.push('');
|
|
34422
|
+
sections.push('## Implementation Context');
|
|
34423
|
+
sections.push(...icBody);
|
|
34424
|
+
}
|
|
34272
34425
|
sections.push(...decisionLines(spec.decisions, '## Decisions & Ruled-Out Alternatives'));
|
|
34273
34426
|
if (spec.outcomes?.length) {
|
|
34274
34427
|
sections.push('');
|
|
@@ -34556,6 +34709,26 @@ function buildWriterDescription(spec) {
|
|
|
34556
34709
|
sections.push('Write your deliverables to /mnt/session/outputs/. Iterate until the grader is satisfied.');
|
|
34557
34710
|
return sections.join('\n');
|
|
34558
34711
|
}
|
|
34712
|
+
/**
|
|
34713
|
+
* May this implementation context set grading criteria?
|
|
34714
|
+
*
|
|
34715
|
+
* The rubric turns `risks` into must-handle criteria whose absence is a FAIL, and
|
|
34716
|
+
* `verificationSuggestions` into checks the grader runs. That is fine when the analyzer wrote them:
|
|
34717
|
+
* it is a separate party from the agent being graded. It is not fine when the agent wrote them,
|
|
34718
|
+
* because then the party under judgment is drafting its own rubric — it could file a trivial risk it
|
|
34719
|
+
* has already handled, or omit the hard one, and the grader would never know the difference.
|
|
34720
|
+
*
|
|
34721
|
+
* Agent-supplied context still reaches the implementing agent as prose; it just cannot bind the
|
|
34722
|
+
* grader until a human has looked at it and promoted it. Anything without a `source` is analyzer
|
|
34723
|
+
* output from before this distinction existed, and keeps its old behavior.
|
|
34724
|
+
*/
|
|
34725
|
+
function bindsGradingCriteria(ic) {
|
|
34726
|
+
if (!ic)
|
|
34727
|
+
return false;
|
|
34728
|
+
if (ic.source !== 'agent')
|
|
34729
|
+
return true;
|
|
34730
|
+
return !!ic.promotedAt;
|
|
34731
|
+
}
|
|
34559
34732
|
/**
|
|
34560
34733
|
* Build the grader-facing rubric — what the independent grader reads. Maps to the
|
|
34561
34734
|
* `rubric` field of a `user.define_outcome` event. Every criterion is written to
|
|
@@ -34580,7 +34753,7 @@ function buildGraderRubric(spec, opts = {}) {
|
|
|
34580
34753
|
}
|
|
34581
34754
|
// Edge cases → must-handle criteria (structured edge cases + implementation-context risks)
|
|
34582
34755
|
const edgeCases = (spec.edgeCases ?? []).filter((ec) => ec.scenario?.trim() || ec.expectedBehavior?.trim());
|
|
34583
|
-
const risks = (spec.implementationContext?.risks ?? []).filter((r) => r?.trim());
|
|
34756
|
+
const risks = (bindsGradingCriteria(spec.implementationContext) ? spec.implementationContext?.risks ?? [] : []).filter((r) => r?.trim());
|
|
34584
34757
|
if (edgeCases.length || risks.length) {
|
|
34585
34758
|
sections.push('');
|
|
34586
34759
|
sections.push('## Edge cases (must handle)');
|
|
@@ -34614,9 +34787,11 @@ function buildGraderRubric(spec, opts = {}) {
|
|
|
34614
34787
|
const checks = [];
|
|
34615
34788
|
for (const c of toVerificationChecks(spec.verification))
|
|
34616
34789
|
checks.push(`[${c.kind}] ${renderCheckLine(c)}`);
|
|
34617
|
-
|
|
34618
|
-
|
|
34619
|
-
|
|
34790
|
+
if (bindsGradingCriteria(spec.implementationContext)) {
|
|
34791
|
+
for (const t of spec.implementationContext?.verificationSuggestions ?? [])
|
|
34792
|
+
if (t?.trim())
|
|
34793
|
+
checks.push(`[suggested] ${t}`);
|
|
34794
|
+
}
|
|
34620
34795
|
if (checks.length) {
|
|
34621
34796
|
sections.push('');
|
|
34622
34797
|
sections.push('## Checks to run (produce the evidence yourself)');
|
|
@@ -34754,6 +34929,7 @@ function readIntentMeta(filePath) {
|
|
|
34754
34929
|
version: Number.isFinite(version) && version >= 1 ? version : 1,
|
|
34755
34930
|
status: typeof data.status === 'string' && data.status.trim() ? data.status.trim() : 'draft',
|
|
34756
34931
|
created: typeof data.created === 'string' ? data.created : undefined,
|
|
34932
|
+
specVersion: typeof data.specVersion === 'string' && data.specVersion.trim() ? data.specVersion.trim() : undefined,
|
|
34757
34933
|
};
|
|
34758
34934
|
}
|
|
34759
34935
|
catch {
|
|
@@ -34797,6 +34973,7 @@ function parseIntentMarkdown(content, fallbackId = 'intent') {
|
|
|
34797
34973
|
version: Number.isFinite(version) && version >= 1 ? version : 1,
|
|
34798
34974
|
objective: data.objective || extractSection(sections, 'Objective') || '',
|
|
34799
34975
|
currentState: extractSection(sections, 'Current State') || undefined,
|
|
34976
|
+
implementationContext: extractSection(sections, 'Implementation Context') || undefined,
|
|
34800
34977
|
title: data.title || data.userGoal || extractTitle(body) || 'Untitled Intent',
|
|
34801
34978
|
stageName: data.stage || undefined,
|
|
34802
34979
|
severity: data.severity || undefined,
|
|
@@ -35040,6 +35217,108 @@ function hasVerificationContent(v) {
|
|
|
35040
35217
|
}
|
|
35041
35218
|
|
|
35042
35219
|
|
|
35220
|
+
/***/ }),
|
|
35221
|
+
|
|
35222
|
+
/***/ 1635:
|
|
35223
|
+
/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => {
|
|
35224
|
+
|
|
35225
|
+
"use strict";
|
|
35226
|
+
|
|
35227
|
+
/**
|
|
35228
|
+
* Input schema for the `record_outcome_measurement` tool.
|
|
35229
|
+
*
|
|
35230
|
+
* **This mirrors `lib/outcomeMeasurementRecord.ts` in the web app and must stay in step
|
|
35231
|
+
* with it.** It cannot import it: this package publishes to npm as a standalone bundle
|
|
35232
|
+
* (ncc, no workspace deps), so reaching into the app's `lib/` would drag the web app into
|
|
35233
|
+
* the published artifact.
|
|
35234
|
+
*
|
|
35235
|
+
* Duplication is the cost, drift is the risk, and drift already happened once — the first
|
|
35236
|
+
* version of this tool accepted requests the API then rejected: both outcome selectors
|
|
35237
|
+
* omitted, a rolling window with no duration, a fixed window with no bounds, a free-form
|
|
35238
|
+
* `evaluatedAt`, and unbounded strings where the API caps length. Every one of those cost
|
|
35239
|
+
* an agent a network round trip to learn something it could have been told locally.
|
|
35240
|
+
* `measurement-schema.test.ts` pins the cases so the next drift fails in CI instead.
|
|
35241
|
+
*
|
|
35242
|
+
* Bounds below are the API's: 500 chars for text values, 100 for short identifiers, 50 for
|
|
35243
|
+
* units and source labels.
|
|
35244
|
+
*/
|
|
35245
|
+
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
|
35246
|
+
exports.recordOutcomeMeasurementInputSchema = exports.provenanceShape = exports.expectationShape = exports.windowShape = exports.queryRefShape = exports.windowKindEnum = exports.comparatorEnum = void 0;
|
|
35247
|
+
exports.outcomeSelectorError = outcomeSelectorError;
|
|
35248
|
+
const zod_1 = __nccwpck_require__(924);
|
|
35249
|
+
const MAX_TEXT = 500;
|
|
35250
|
+
const MAX_SHORT = 100;
|
|
35251
|
+
const MAX_UNIT = 50;
|
|
35252
|
+
exports.comparatorEnum = zod_1.z.enum(['gt', 'gte', 'lt', 'lte', 'eq']);
|
|
35253
|
+
exports.windowKindEnum = zod_1.z.enum(['rolling', 'fixed', 'since_ship']);
|
|
35254
|
+
exports.queryRefShape = zod_1.z.object({
|
|
35255
|
+
kind: zod_1.z.string().trim().min(1).max(MAX_SHORT).describe('How to interpret the id, e.g. "saved_insight"'),
|
|
35256
|
+
id: zod_1.z.string().trim().min(1).max(MAX_TEXT).describe('Stable id of the query. An id, never a name — names get renamed and the record then points nowhere.'),
|
|
35257
|
+
});
|
|
35258
|
+
/**
|
|
35259
|
+
* Refinements do not appear in the JSON Schema the model sees, so the requirement is also
|
|
35260
|
+
* stated in each `describe`. The refinement is what stops a malformed window reaching the
|
|
35261
|
+
* API; the description is what stops the model producing one in the first place.
|
|
35262
|
+
*/
|
|
35263
|
+
exports.windowShape = zod_1.z
|
|
35264
|
+
.object({
|
|
35265
|
+
kind: exports.windowKindEnum.describe('How the window is bounded'),
|
|
35266
|
+
duration: zod_1.z.string().trim().min(1).max(MAX_SHORT).optional().describe('REQUIRED when kind is "rolling", e.g. "28d"'),
|
|
35267
|
+
start: zod_1.z.string().trim().min(1).max(MAX_SHORT).optional().describe('REQUIRED when kind is "fixed"'),
|
|
35268
|
+
end: zod_1.z.string().trim().min(1).max(MAX_SHORT).optional().describe('REQUIRED when kind is "fixed"'),
|
|
35269
|
+
})
|
|
35270
|
+
.refine(w => w.kind !== 'rolling' || !!w.duration, {
|
|
35271
|
+
message: 'A rolling window requires a duration',
|
|
35272
|
+
path: ['duration'],
|
|
35273
|
+
})
|
|
35274
|
+
.refine(w => w.kind !== 'fixed' || (!!w.start && !!w.end), {
|
|
35275
|
+
message: 'A fixed window requires both start and end',
|
|
35276
|
+
path: ['start'],
|
|
35277
|
+
});
|
|
35278
|
+
exports.expectationShape = zod_1.z.object({
|
|
35279
|
+
operator: exports.comparatorEnum,
|
|
35280
|
+
target: zod_1.z.number().finite(),
|
|
35281
|
+
unit: zod_1.z.string().trim().max(MAX_UNIT).optional(),
|
|
35282
|
+
});
|
|
35283
|
+
exports.provenanceShape = zod_1.z.object({
|
|
35284
|
+
provider: zod_1.z.string().trim().min(1).max(MAX_SHORT).describe('System the number came from, e.g. "posthog"'),
|
|
35285
|
+
queryRef: exports.queryRefShape,
|
|
35286
|
+
queryVersion: zod_1.z.string().trim().min(1).max(MAX_SHORT).optional().describe('Version or hash of the query, so a later edit to it is detectable'),
|
|
35287
|
+
window: exports.windowShape,
|
|
35288
|
+
evaluatedAt: zod_1.z.string().datetime().optional().describe('ISO-8601 timestamp of when you ran the query, e.g. "2026-08-25T12:00:00Z". Defaults to now.'),
|
|
35289
|
+
expectation: exports.expectationShape.optional().describe('The threshold the outcome named. Supply this together with numericValue and the server decides `met` itself rather than taking your word for it.'),
|
|
35290
|
+
rawResponse: zod_1.z.unknown().optional().describe('Snapshot of what the provider returned, so the number survives the query being changed or deleted'),
|
|
35291
|
+
recordedByTool: zod_1.z.string().trim().min(1).max(MAX_SHORT).optional().describe('Which tool recorded this, e.g. "claude-code"'),
|
|
35292
|
+
});
|
|
35293
|
+
/** The tool's field map, in the ZodRawShape form `registerTool` expects. */
|
|
35294
|
+
exports.recordOutcomeMeasurementInputSchema = {
|
|
35295
|
+
intentId: zod_1.z.string().trim().min(1).describe('The intent whose outcome you measured'),
|
|
35296
|
+
outcomeId: zod_1.z.string().trim().min(1).optional().describe('Stable id of the outcome, from get_intent. Preferred over outcomeIndex.'),
|
|
35297
|
+
outcomeIndex: zod_1.z.number().int().nonnegative().optional().describe('Positional fallback when the outcome has no id. Exactly one of outcomeId or outcomeIndex is required.'),
|
|
35298
|
+
actualValue: zod_1.z.string().trim().min(1).max(MAX_TEXT).describe('What you measured, as displayable text, e.g. "12.4%" or "3m 20s"'),
|
|
35299
|
+
numericValue: zod_1.z.number().finite().optional().describe('The same value as a number. Required for the server to check an expectation — without it the verdict can only be self-reported.'),
|
|
35300
|
+
baselineValue: zod_1.z.string().trim().max(MAX_TEXT).optional().describe('What it was before, if you know it'),
|
|
35301
|
+
unit: zod_1.z.string().trim().max(MAX_UNIT).optional().describe('Unit of the value, e.g. "%", "seconds", "signups/day"'),
|
|
35302
|
+
met: zod_1.z.boolean().optional().describe('Only for when there is no computable expectation. Stored as a self-report, not as a checked fact.'),
|
|
35303
|
+
source: zod_1.z.string().trim().max(MAX_UNIT).optional().describe('Where the number came from, e.g. "posthog"'),
|
|
35304
|
+
measurement: exports.provenanceShape.optional().describe('Provenance. Omit only if you genuinely cannot say where the number came from.'),
|
|
35305
|
+
};
|
|
35306
|
+
/**
|
|
35307
|
+
* The one constraint that cannot live in the field map.
|
|
35308
|
+
*
|
|
35309
|
+
* `registerTool` takes a ZodRawShape and builds the object itself, so an object-level
|
|
35310
|
+
* `.refine()` across two sibling fields has nowhere to attach. Checking it in the handler
|
|
35311
|
+
* keeps the failure local and the message identical to the API's, instead of spending a
|
|
35312
|
+
* round trip to be told the same thing.
|
|
35313
|
+
*/
|
|
35314
|
+
function outcomeSelectorError(input) {
|
|
35315
|
+
if (input.outcomeId == null && input.outcomeIndex == null) {
|
|
35316
|
+
return 'outcomeId (preferred) or outcomeIndex is required. Call get_intent to read the outcome ids on this intent.';
|
|
35317
|
+
}
|
|
35318
|
+
return null;
|
|
35319
|
+
}
|
|
35320
|
+
|
|
35321
|
+
|
|
35043
35322
|
/***/ }),
|
|
35044
35323
|
|
|
35045
35324
|
/***/ 4681:
|
|
@@ -35092,6 +35371,111 @@ function mergePathmodeSection(existing, section) {
|
|
|
35092
35371
|
}
|
|
35093
35372
|
|
|
35094
35373
|
|
|
35374
|
+
/***/ }),
|
|
35375
|
+
|
|
35376
|
+
/***/ 4239:
|
|
35377
|
+
/***/ ((__unused_webpack_module, exports) => {
|
|
35378
|
+
|
|
35379
|
+
"use strict";
|
|
35380
|
+
|
|
35381
|
+
/**
|
|
35382
|
+
* Push a saved spec to the workspace, in the order that makes a retry repair rather than duplicate.
|
|
35383
|
+
*
|
|
35384
|
+
* Three legs, and the split between them is the whole design:
|
|
35385
|
+
*
|
|
35386
|
+
* 1. **Identity** (create or update). The only leg allowed to fail the save. It settles what this
|
|
35387
|
+
* spec IS and returns the server's opaque version token.
|
|
35388
|
+
* 2. **The file write**, via `onIdentitySettled`, immediately after leg 1 and BEFORE the rest.
|
|
35389
|
+
* 3. **Enrichment** (decisions, implementation context). These report; they never fail the save.
|
|
35390
|
+
*
|
|
35391
|
+
* Why the file cannot wait until every leg succeeds: the write is what puts `specVersion` on disk,
|
|
35392
|
+
* and `specVersion` on disk is what makes the NEXT save an update. Defer the write and a failure in
|
|
35393
|
+
* leg 3 leaves a cloud record with no local token — so the next save looks like a first save and
|
|
35394
|
+
* creates a SECOND intent. Refusing to write would not prevent divergence, it would manufacture
|
|
35395
|
+
* duplicates, which is worse than the state it was avoiding.
|
|
35396
|
+
*
|
|
35397
|
+
* Enrichment goes through dedicated endpoints rather than fields on create because each is validated
|
|
35398
|
+
* there: evidence ids are checked against the workspace so provenance cannot be fabricated, and
|
|
35399
|
+
* implementation context is stamped with its source so unpromoted agent content stays out of the
|
|
35400
|
+
* grading rubric. Riding along on create would bypass both.
|
|
35401
|
+
*
|
|
35402
|
+
* A failed enrichment leg is recoverable by saving again: its content is already in the local file,
|
|
35403
|
+
* and the retry is an update.
|
|
35404
|
+
*/
|
|
35405
|
+
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
|
35406
|
+
exports.pushSpec = pushSpec;
|
|
35407
|
+
async function pushSpec(input) {
|
|
35408
|
+
const { client, id, existingSpecVersion, payload } = input;
|
|
35409
|
+
let canonicalId;
|
|
35410
|
+
let specVersion;
|
|
35411
|
+
let sourceUrl;
|
|
35412
|
+
try {
|
|
35413
|
+
// On create the local id travels with the spec. Promotion must not change identity: a
|
|
35414
|
+
// keyless author may already have stamped `intent/<uuid>` on a branch, and a new id would
|
|
35415
|
+
// quietly break the merge resolution that stamp exists for.
|
|
35416
|
+
let saved;
|
|
35417
|
+
if (existingSpecVersion) {
|
|
35418
|
+
saved = await client.updateIntent(id, { ...payload, expectedVersion: existingSpecVersion });
|
|
35419
|
+
}
|
|
35420
|
+
else {
|
|
35421
|
+
try {
|
|
35422
|
+
saved = await client.createIntent({ ...payload, id });
|
|
35423
|
+
}
|
|
35424
|
+
catch (e) {
|
|
35425
|
+
// The create may have COMMITTED and lost its response — a dropped connection after
|
|
35426
|
+
// the insert. Retrying then hits INTENT_ID_TAKEN forever, and because specVersion
|
|
35427
|
+
// never reached the file every later save also looks like a first save. Since WE
|
|
35428
|
+
// chose the id, a taken id in our own workspace is our own record: adopt it.
|
|
35429
|
+
if (e?.code === 'INTENT_ID_TAKEN' && client.getIntent) {
|
|
35430
|
+
saved = await client.getIntent(id);
|
|
35431
|
+
}
|
|
35432
|
+
else {
|
|
35433
|
+
throw e;
|
|
35434
|
+
}
|
|
35435
|
+
}
|
|
35436
|
+
}
|
|
35437
|
+
canonicalId = saved.id || id;
|
|
35438
|
+
specVersion = saved.specVersion;
|
|
35439
|
+
sourceUrl = `https://www.pathmode.io/intent/${canonicalId}`;
|
|
35440
|
+
}
|
|
35441
|
+
catch (e) {
|
|
35442
|
+
// Keep the structured detail. PRODUCT_REQUIRED carries the candidate products so the agent can
|
|
35443
|
+
// ask which one and retry; a flattened message leaves it with nothing to act on.
|
|
35444
|
+
const products = Array.isArray(e?.body?.products) ? e.body.products : undefined;
|
|
35445
|
+
return {
|
|
35446
|
+
ok: false,
|
|
35447
|
+
error: String(e?.message || e),
|
|
35448
|
+
...(e?.code ? { code: e.code } : {}),
|
|
35449
|
+
...(products ? { products } : {}),
|
|
35450
|
+
};
|
|
35451
|
+
}
|
|
35452
|
+
input.onIdentitySettled({ canonicalId, specVersion, sourceUrl });
|
|
35453
|
+
const didNotTravel = [];
|
|
35454
|
+
for (const d of input.decisions ?? []) {
|
|
35455
|
+
try {
|
|
35456
|
+
await client.recordDecision(canonicalId, { choice: d.choice, reason: d.reason, ruledOut: d.ruledOut });
|
|
35457
|
+
}
|
|
35458
|
+
catch {
|
|
35459
|
+
didNotTravel.push(`decision "${d.choice.slice(0, 40)}" — save again to retry`);
|
|
35460
|
+
}
|
|
35461
|
+
}
|
|
35462
|
+
if (input.implementationContext?.trim()) {
|
|
35463
|
+
try {
|
|
35464
|
+
// State the version this was gathered against, so a spec that moved between the
|
|
35465
|
+
// identity write and here is a 409 rather than context describing a spec that is gone.
|
|
35466
|
+
await client.saveImplementationContext(canonicalId, {
|
|
35467
|
+
currentBehavior: input.implementationContext,
|
|
35468
|
+
...(specVersion ? { expectedSpecVersion: specVersion } : {}),
|
|
35469
|
+
});
|
|
35470
|
+
}
|
|
35471
|
+
catch {
|
|
35472
|
+
didNotTravel.push('implementation context — call record_implementation_context to retry');
|
|
35473
|
+
}
|
|
35474
|
+
}
|
|
35475
|
+
return { ok: true, canonicalId, specVersion, sourceUrl, didNotTravel };
|
|
35476
|
+
}
|
|
35477
|
+
|
|
35478
|
+
|
|
35095
35479
|
/***/ }),
|
|
35096
35480
|
|
|
35097
35481
|
/***/ 3079:
|
|
@@ -39224,7 +39608,7 @@ const EMPTY_COMPLETION_RESULT = {
|
|
|
39224
39608
|
|
|
39225
39609
|
/***/ }),
|
|
39226
39610
|
|
|
39227
|
-
/***/
|
|
39611
|
+
/***/ 9477:
|
|
39228
39612
|
/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) {
|
|
39229
39613
|
|
|
39230
39614
|
"use strict";
|
|
@@ -65268,7 +65652,7 @@ module.exports = /*#__PURE__*/JSON.parse('{"$schema":"http://json-schema.org/dra
|
|
|
65268
65652
|
/***/ ((module) => {
|
|
65269
65653
|
|
|
65270
65654
|
"use strict";
|
|
65271
|
-
module.exports = /*#__PURE__*/JSON.parse('{"name":"@pathmode/mcp-server","version":"1.
|
|
65655
|
+
module.exports = /*#__PURE__*/JSON.parse('{"name":"@pathmode/mcp-server","version":"1.14.0","publishConfig":{"access":"public"},"mcpName":"io.github.pathmodeio/mcp-server","description":"Deterministic intent preflight before your agent builds: six calibrated gates, keyless, no model call. Draft and sharpen specs in conversation, or connect a Pathmode workspace to sync intent and evidence across a team.","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"},"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"}}');
|
|
65272
65656
|
|
|
65273
65657
|
/***/ })
|
|
65274
65658
|
|
|
@@ -65358,7 +65742,9 @@ if (typeof globalThis.fetch === 'undefined') {
|
|
|
65358
65742
|
}
|
|
65359
65743
|
}
|
|
65360
65744
|
const mcp_js_1 = __nccwpck_require__(3886);
|
|
65361
|
-
const
|
|
65745
|
+
const crypto_1 = __nccwpck_require__(6982);
|
|
65746
|
+
const push_spec_1 = __nccwpck_require__(4239);
|
|
65747
|
+
const stdio_js_1 = __nccwpck_require__(9477);
|
|
65362
65748
|
const zod_1 = __nccwpck_require__(924);
|
|
65363
65749
|
const path_1 = __nccwpck_require__(6928);
|
|
65364
65750
|
const fs_1 = __nccwpck_require__(9896);
|
|
@@ -65369,6 +65755,7 @@ const save_policy_1 = __nccwpck_require__(6137);
|
|
|
65369
65755
|
const intent_compiler_1 = __nccwpck_require__(6488);
|
|
65370
65756
|
const pathmode_section_1 = __nccwpck_require__(4681);
|
|
65371
65757
|
const setup_1 = __nccwpck_require__(8294);
|
|
65758
|
+
const measurement_schema_1 = __nccwpck_require__(1635);
|
|
65372
65759
|
const install_skills_1 = __nccwpck_require__(3783);
|
|
65373
65760
|
const cli_info_1 = __nccwpck_require__(7198);
|
|
65374
65761
|
// Server version is sourced from package.json so the version reported to MCP
|
|
@@ -65460,6 +65847,64 @@ function startMcpServer() {
|
|
|
65460
65847
|
const cloudOnly = isLocalMode
|
|
65461
65848
|
? { registerTool: (() => undefined) }
|
|
65462
65849
|
: server;
|
|
65850
|
+
// ── Per-tool-invocation signal ────────────────────────────────
|
|
65851
|
+
//
|
|
65852
|
+
// LOCAL MODE EMITS NOTHING. Keyless installs make no network request at all, which is a
|
|
65853
|
+
// published promise (see LOCAL_MODE_INSTRUCTIONS above and pathmode.io/privacy), not an
|
|
65854
|
+
// implementation detail. The gate is the early return below; there is no other path to
|
|
65855
|
+
// recordToolCall.
|
|
65856
|
+
//
|
|
65857
|
+
// In cloud mode the launch handshake said a client started and nothing said whether it
|
|
65858
|
+
// did anything. check_intent_readiness on an inline spec, intent_save and intent_export
|
|
65859
|
+
// compute and write entirely on this machine, so even with a valid key they reached no
|
|
65860
|
+
// endpoint and produced no `api_call` — 140 launches and zero calls in 13.5 hours reads
|
|
65861
|
+
// identically whether the preflight loop was hammered or never touched.
|
|
65862
|
+
//
|
|
65863
|
+
// Wired at the registration seam rather than inside ~25 handlers: a tool added later is
|
|
65864
|
+
// measured because it was registered, not because someone remembered to instrument it.
|
|
65865
|
+
// Both entry points are patched because McpServer.tool() and .registerTool() each build
|
|
65866
|
+
// the handler directly (neither delegates to the other), so this wraps every tool
|
|
65867
|
+
// exactly once. Sends a name and an outcome, never an argument.
|
|
65868
|
+
//
|
|
65869
|
+
// Known gap: the SDK rejects arguments that fail the tool's zod schema before the handler
|
|
65870
|
+
// runs, so a malformed call is answered with -32602 and reports nothing. What this
|
|
65871
|
+
// measures is "a tool executed", not "a tool was attempted". Seeing the malformed
|
|
65872
|
+
// attempts would mean hooking the tools/call request handler instead, which is private
|
|
65873
|
+
// SDK surface — worth doing only if the executed-call numbers turn out to need it.
|
|
65874
|
+
function instrumentToolTelemetry() {
|
|
65875
|
+
if (isLocalMode || !client)
|
|
65876
|
+
return;
|
|
65877
|
+
const cloud = client;
|
|
65878
|
+
// Fire-and-forget on both paths, never awaited: a tool's result must not wait on,
|
|
65879
|
+
// or be lost to, our analytics. recordToolCall swallows its own failures.
|
|
65880
|
+
const wrap = (name, handler) => async (...args) => {
|
|
65881
|
+
try {
|
|
65882
|
+
const result = await handler(...args);
|
|
65883
|
+
void cloud.recordToolCall(name, result?.isError ? 'error' : 'ok');
|
|
65884
|
+
return result;
|
|
65885
|
+
}
|
|
65886
|
+
catch (e) {
|
|
65887
|
+
// The SDK turns a thrown handler error into an isError result for the client;
|
|
65888
|
+
// we record the outcome and stay out of the way.
|
|
65889
|
+
void cloud.recordToolCall(name, 'error');
|
|
65890
|
+
throw e;
|
|
65891
|
+
}
|
|
65892
|
+
};
|
|
65893
|
+
// The handler is the last argument of both signatures — tool(name, ...rest, cb) and
|
|
65894
|
+
// registerTool(name, config, cb).
|
|
65895
|
+
const patch = (method) => {
|
|
65896
|
+
const original = server[method].bind(server);
|
|
65897
|
+
server[method] = (...args) => {
|
|
65898
|
+
const handler = args[args.length - 1];
|
|
65899
|
+
if (typeof handler !== 'function')
|
|
65900
|
+
return original(...args);
|
|
65901
|
+
return original(...args.slice(0, -1), wrap(String(args[0]), handler));
|
|
65902
|
+
};
|
|
65903
|
+
};
|
|
65904
|
+
patch('tool');
|
|
65905
|
+
patch('registerTool');
|
|
65906
|
+
}
|
|
65907
|
+
instrumentToolTelemetry();
|
|
65463
65908
|
function normalizeText(value) {
|
|
65464
65909
|
return (value || '').trim();
|
|
65465
65910
|
}
|
|
@@ -66033,7 +66478,7 @@ function startMcpServer() {
|
|
|
66033
66478
|
// ============================================================
|
|
66034
66479
|
cloudOnly.registerTool('update_intent_status', {
|
|
66035
66480
|
title: 'Update Intent Status',
|
|
66036
|
-
description: 'Update the status of an intent.
|
|
66481
|
+
description: 'Update the status of an intent. **If the change ships through a pull request, do NOT use this to mark it shipped** — stamp the intent reference on the branch or PR body and let the merge do it, because the merge grades the real diff and an intent already at shipped is skipped by that grading. Use this for work that will never appear in a PR, and for the shipped→verified transition once the outcome is confirmed in production. For shipped/verified transitions, the response includes a verification checklist of outcomes, constitution rules, and health metrics that should be confirmed.',
|
|
66037
66482
|
inputSchema: {
|
|
66038
66483
|
intentId: zod_1.z.string().describe('The intent ID to update'),
|
|
66039
66484
|
status: zod_1.z.enum(['draft', 'validated', 'approved', 'shipped', 'verified']).describe('The new status'),
|
|
@@ -66080,6 +66525,50 @@ function startMcpServer() {
|
|
|
66080
66525
|
}]
|
|
66081
66526
|
};
|
|
66082
66527
|
});
|
|
66528
|
+
cloudOnly.registerTool('record_implementation_context', {
|
|
66529
|
+
title: 'Record Implementation Context',
|
|
66530
|
+
description: 'Hand back what the repo actually looks like where an intent lands: the files and modules involved, how it behaves today, what a change would risk, and how to verify it. You are already in the working tree, so read it rather than guessing — you see more than our repo analyzer can, including uncommitted work. Advisory by design: it never changes the readiness verdict. Its job is to stop the NEXT agent from rediscovering the codebase, and to surface the case where the thing being specified half-exists already.',
|
|
66531
|
+
inputSchema: {
|
|
66532
|
+
intentId: zod_1.z.string().describe('The intent this context describes'),
|
|
66533
|
+
relevantAreas: zod_1.z.array(zod_1.z.object({
|
|
66534
|
+
path: zod_1.z.string().describe('File or directory path'),
|
|
66535
|
+
reason: zod_1.z.string().describe('Why this change touches it'),
|
|
66536
|
+
})).optional().describe('Where the change lands. Max 100.'),
|
|
66537
|
+
currentBehavior: zod_1.z.string().optional().describe('How the code behaves today where this lands, read from the tree rather than assumed.'),
|
|
66538
|
+
liftEstimate: zod_1.z.enum(['low', 'medium', 'high']).optional().describe('Rough size of the change.'),
|
|
66539
|
+
risks: zod_1.z.array(zod_1.z.string()).optional().describe('What a change here could break. Reaches the implementing agent as prose; it does NOT become a grading criterion until a human promotes it, because you would otherwise be writing the rubric you are graded against.'),
|
|
66540
|
+
verificationSuggestions: zod_1.z.array(zod_1.z.string()).optional().describe('Checks worth running. Same promotion rule as risks.'),
|
|
66541
|
+
expectedSpecVersion: zod_1.z.string().optional().describe('The spec content hash you gathered this against, from a previous read. If the spec has moved since, the write is refused rather than attaching a description of a spec that no longer exists.'),
|
|
66542
|
+
},
|
|
66543
|
+
}, async ({ intentId, ...input }) => {
|
|
66544
|
+
if (isLocalMode) {
|
|
66545
|
+
// Not a dead end: the local path exists and is lossless, because intent_save carries the
|
|
66546
|
+
// whole spec rather than read-modify-writing a file this tool would have to reconstruct.
|
|
66547
|
+
return {
|
|
66548
|
+
content: [{
|
|
66549
|
+
type: 'text',
|
|
66550
|
+
text: 'In keyless local mode there is no separate call for this. Pass the same text to `intent_save` as `implementationContext` and it lands in the `## Implementation Context` section of intent.md.',
|
|
66551
|
+
}],
|
|
66552
|
+
};
|
|
66553
|
+
}
|
|
66554
|
+
try {
|
|
66555
|
+
const result = await requireCloudClient().saveImplementationContext(intentId, input);
|
|
66556
|
+
const ctx = result?.implementationContext ?? {};
|
|
66557
|
+
const areas = Array.isArray(ctx.relevantAreas) ? ctx.relevantAreas.length : 0;
|
|
66558
|
+
return {
|
|
66559
|
+
content: [{
|
|
66560
|
+
type: 'text',
|
|
66561
|
+
text: `✓ Implementation context recorded for ${intentId} (${areas} area(s), spec version ${result?.specVersion ?? 'unknown'}).\n\nIt reaches the implementing agent as context. Risks and verification suggestions stay out of the grading rubric until a human promotes them.`,
|
|
66562
|
+
}],
|
|
66563
|
+
};
|
|
66564
|
+
}
|
|
66565
|
+
catch (e) {
|
|
66566
|
+
return {
|
|
66567
|
+
content: [{ type: 'text', text: `Failed to record implementation context: ${e?.message || e}` }],
|
|
66568
|
+
isError: true,
|
|
66569
|
+
};
|
|
66570
|
+
}
|
|
66571
|
+
});
|
|
66083
66572
|
cloudOnly.registerTool('record_implementation_finding', {
|
|
66084
66573
|
title: 'Record Implementation Finding',
|
|
66085
66574
|
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.',
|
|
@@ -66105,14 +66594,47 @@ function startMcpServer() {
|
|
|
66105
66594
|
}]
|
|
66106
66595
|
};
|
|
66107
66596
|
});
|
|
66597
|
+
cloudOnly.registerTool('record_outcome_measurement', {
|
|
66598
|
+
title: 'Record Outcome Measurement',
|
|
66599
|
+
description: 'Record what actually happened to one of an intent\'s outcomes after it shipped. Use this to close the loop: the spec said what should change, this says whether it did. NOT for spec corrections — those are record_implementation_finding. Send numericValue together with measurement.expectation and the server computes whether the target was met, which is what makes the record auditable rather than an assertion; a `met` you supply yourself is stored but marked self-reported. Point measurement.queryRef at a STABLE id (for PostHog, a saved insight id) rather than a metric name, because names get renamed and the record then points nowhere.',
|
|
66600
|
+
inputSchema: measurement_schema_1.recordOutcomeMeasurementInputSchema,
|
|
66601
|
+
annotations: WRITE_OP,
|
|
66602
|
+
}, async ({ intentId, ...input }) => {
|
|
66603
|
+
if (isLocalMode) {
|
|
66604
|
+
return { content: [{ type: 'text', text: 'Recording outcome measurements requires cloud mode. Use PATHMODE_API_KEY to connect.' }] };
|
|
66605
|
+
}
|
|
66606
|
+
// Caught here rather than at the API: registerTool builds the input object itself,
|
|
66607
|
+
// so this cross-field rule has nowhere to attach in the schema. Same message the
|
|
66608
|
+
// API would return, one round trip earlier.
|
|
66609
|
+
const selectorError = (0, measurement_schema_1.outcomeSelectorError)(input);
|
|
66610
|
+
if (selectorError) {
|
|
66611
|
+
return { content: [{ type: 'text', text: selectorError }], isError: true };
|
|
66612
|
+
}
|
|
66613
|
+
const result = await requireCloudClient().recordOutcomeMeasurement(intentId, input);
|
|
66614
|
+
const basis = result?.metBasis;
|
|
66615
|
+
const verdict = result?.met === null || result?.met === undefined
|
|
66616
|
+
? 'No verdict recorded'
|
|
66617
|
+
: result.met ? 'Target met' : 'Target not met';
|
|
66618
|
+
const how = basis === 'computed'
|
|
66619
|
+
? ' (computed by Pathmode from the expectation you supplied)'
|
|
66620
|
+
: basis === 'self_report'
|
|
66621
|
+
? ' (recorded as your self-report — send numericValue and an expectation next time and Pathmode will check it)'
|
|
66622
|
+
: '';
|
|
66623
|
+
return {
|
|
66624
|
+
content: [{
|
|
66625
|
+
type: 'text',
|
|
66626
|
+
text: `Measurement recorded for intent ${intentId}: ${result?.actualValue ?? input.actualValue}. ${verdict}${how}.`
|
|
66627
|
+
}]
|
|
66628
|
+
};
|
|
66629
|
+
});
|
|
66108
66630
|
cloudOnly.registerTool('create_intent', {
|
|
66109
66631
|
title: 'Create Intent',
|
|
66110
|
-
description: 'Create a new intent spec in the workspace. Requires at minimum a title
|
|
66632
|
+
description: 'Create a new intent spec in the workspace WITHOUT writing a file. Use this only when there is no repository to write into — outside a project directory, intent_save would put intent.md wherever the process happens to be running. Inside a repo, prefer intent_save: it writes the file and syncs to the workspace in one step. Requires at minimum a title and objective; productId is resolved server-side when the workspace has exactly one real product. Returns the created intent with its ID. Use list_intents first to see existing intents and avoid duplicates.',
|
|
66111
66633
|
inputSchema: {
|
|
66112
66634
|
title: zod_1.z.string().describe('Short name for the intent (e.g., "Improve onboarding flow")'),
|
|
66113
66635
|
objective: zod_1.z.string().describe('Why this matters — the problem and who has it'),
|
|
66114
66636
|
currentState: zod_1.z.string().optional().describe('How this works today, before the change: existing behavior, the workaround users rely on, what is broken. Tells implementing agents what must not regress. Omit for genuinely net-new capability.'),
|
|
66115
|
-
productId: zod_1.z.string().describe('Product (Space) ID this intent belongs to.
|
|
66637
|
+
productId: zod_1.z.string().optional().describe('Product (Space) ID this intent belongs to. Omit it and the server resolves it when the workspace has exactly one real product. If it cannot, you get code PRODUCT_REQUIRED with the candidate list: ASK THE USER which product, then retry with an explicit id. Do not guess — a GitHub binding hangs off the product, so the product decides which repository the PR stamp and merge verification apply to, and a wrong guess surfaces much later as a merge that silently fails to verify.'),
|
|
66116
66638
|
outcomes: zod_1.z.array(zod_1.z.string()).optional().describe('Observable, testable state changes'),
|
|
66117
66639
|
constraints: zod_1.z.array(zod_1.z.string()).optional().describe('Hard limits the implementation must respect'),
|
|
66118
66640
|
healthMetrics: zod_1.z.array(zod_1.z.string()).optional().describe('What to monitor after shipping'),
|
|
@@ -66335,7 +66857,7 @@ function startMcpServer() {
|
|
|
66335
66857
|
role: 'user',
|
|
66336
66858
|
content: {
|
|
66337
66859
|
type: 'text',
|
|
66338
|
-
text: `I need to implement intent ${intentId}. Please:\n1. Use the get_agent_prompt tool to fetch the full execution prompt for this intent\n2. Use get_constitution to check for workspace constraints I must respect\n3. Review the intent details and create an implementation plan\n4. After implementation, use verify_implementation to AI-grade your work against the spec\
|
|
66860
|
+
text: `I need to implement intent ${intentId}. Please:\n1. Use the get_agent_prompt tool to fetch the full execution prompt for this intent\n2. Use get_constitution to check for workspace constraints I must respect\n3. Review the intent details and create an implementation plan\n4. Name the work so the merge can find it: branch \`intent/${intentId}\`, or put \`pathmode:${intentId}\` in the pull request body\n5. After implementation, use verify_implementation to AI-grade your work against the spec\n6. If this change goes through a pull request, STOP there — the merge grades the real diff and moves the intent to Shipped. Do NOT call update_intent_status: an intent already at shipped is skipped by the merge, so an early flip replaces a diff-backed verdict with an unverified one\n7. Only if there will be no pull request, use update_intent_status to mark it "shipped"\n8. Use log_implementation_note to document key technical decisions`,
|
|
66339
66861
|
},
|
|
66340
66862
|
}],
|
|
66341
66863
|
};
|
|
@@ -66383,6 +66905,8 @@ function startMcpServer() {
|
|
|
66383
66905
|
title: zod_1.z.string().describe('Short name for the intent'),
|
|
66384
66906
|
objective: zod_1.z.string().describe('Why this matters — the problem and who has it'),
|
|
66385
66907
|
currentState: zod_1.z.string().optional().describe('How this works today, before the change — existing behavior and workarounds, so the implementation knows what must not regress. Omit for net-new capability.'),
|
|
66908
|
+
implementationContext: zod_1.z.string().optional().describe('What the repo actually looks like where this change lands: the files and modules involved, what already exists, what it would touch, how to verify it. You are in the working tree — go read it. Markdown, sub-headings welcome. Advisory: it never changes the readiness verdict, it just stops the implementing agent from rediscovering the codebase.'),
|
|
66909
|
+
productId: zod_1.z.string().optional().describe('Which product this intent belongs to (cloud mode only). Omit it and the server resolves it when the workspace has exactly one real product; if it cannot, the save fails with PRODUCT_REQUIRED and the candidate list, and you should ask the user which one and retry with an explicit id rather than guessing — a GitHub binding hangs off the product, so it decides which repository merge verification applies to.'),
|
|
66386
66910
|
outcomes: zod_1.z.array(zod_1.z.string()).describe('Observable, testable state changes'),
|
|
66387
66911
|
decisions: zod_1.z.array(zod_1.z.object({
|
|
66388
66912
|
choice: zod_1.z.string(),
|
|
@@ -66422,6 +66946,21 @@ function startMcpServer() {
|
|
|
66422
66946
|
}],
|
|
66423
66947
|
};
|
|
66424
66948
|
});
|
|
66949
|
+
/**
|
|
66950
|
+
* Tool input → IntentFields.
|
|
66951
|
+
*
|
|
66952
|
+
* The tool parameter is called `implementationContext` because that is the section it becomes and
|
|
66953
|
+
* the language the agent thinks in. IntentFields reserves that name for the cloud analyzer's
|
|
66954
|
+
* STRUCTURED object, so the prose moves to `implementationContextText` on the way in. Keeping the
|
|
66955
|
+
* split explicit here is what stops a string from reaching code that expects `.relevantAreas`.
|
|
66956
|
+
*/
|
|
66957
|
+
function toIntentFields(spec) {
|
|
66958
|
+
const { implementationContext, ...rest } = spec;
|
|
66959
|
+
return {
|
|
66960
|
+
...rest,
|
|
66961
|
+
...(implementationContext?.trim() ? { implementationContextText: implementationContext } : {}),
|
|
66962
|
+
};
|
|
66963
|
+
}
|
|
66425
66964
|
server.registerTool('check_intent_readiness', {
|
|
66426
66965
|
title: 'Check Intent Readiness (Preflight)',
|
|
66427
66966
|
description: 'Run the deterministic preflight on an intent spec before handing it to an implementation agent. Six calibrated checks — title, objective, outcomes, constraints, edge cases, verification — computed by pure functions: no model call, no network, the same spec always gets the same verdict. Pass a spec inline to check before saving, pass intentId to check a specific saved intent, or pass nothing to check the current intent. A failing verdict names the exact blockers; repair the fields and re-run. This is the gate that runs at preflight.pathmode.io.',
|
|
@@ -66473,7 +67012,7 @@ function startMcpServer() {
|
|
|
66473
67012
|
}],
|
|
66474
67013
|
};
|
|
66475
67014
|
});
|
|
66476
|
-
server.tool('intent_save', 'Save an intent spec to intent.md in the project root.
|
|
67015
|
+
server.tool('intent_save', 'Save an intent spec to intent.md in the project root — the default choice when you are working inside a repository. With a Pathmode API key set it ALSO creates or updates the intent in the workspace and records the sync in the file, so the file is the projection of the record rather than a second copy of it. Re-saving the same intent bumps its version and preserves its status; it never silently replaces a different intent. Use create_intent instead when there is no repository to write into.', {
|
|
66477
67016
|
spec: zod_1.z.object(intentSpecSchema),
|
|
66478
67017
|
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'),
|
|
66479
67018
|
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.'),
|
|
@@ -66484,7 +67023,7 @@ function startMcpServer() {
|
|
|
66484
67023
|
existing,
|
|
66485
67024
|
incomingId: spec.id,
|
|
66486
67025
|
overwrite,
|
|
66487
|
-
mintId: () =>
|
|
67026
|
+
mintId: () => (0, crypto_1.randomUUID)(),
|
|
66488
67027
|
});
|
|
66489
67028
|
if (decision.action === 'refuse') {
|
|
66490
67029
|
return {
|
|
@@ -66504,9 +67043,75 @@ function startMcpServer() {
|
|
|
66504
67043
|
// The preflight verdict travels with the file: stamped into frontmatter on every save,
|
|
66505
67044
|
// recomputed from the spec alone (deterministic). A failing verdict never blocks the
|
|
66506
67045
|
// save — the gate reports, the user decides.
|
|
66507
|
-
const
|
|
66508
|
-
const
|
|
66509
|
-
|
|
67046
|
+
const fields = toIntentFields(spec);
|
|
67047
|
+
const verdict = (0, readiness_1.computeReadinessVerdict)(fields);
|
|
67048
|
+
// The push and the file write are one ordered unit; see push-spec.ts for why the write
|
|
67049
|
+
// cannot wait for the enrichment legs.
|
|
67050
|
+
let canonicalId = id;
|
|
67051
|
+
let specVersion;
|
|
67052
|
+
let sourceUrl;
|
|
67053
|
+
let didNotTravel = [];
|
|
67054
|
+
const writeSpecFile = (opts) => {
|
|
67055
|
+
const content = (0, intent_compiler_1.formatIntentMd)({ ...fields, id: opts.canonicalId }, {
|
|
67056
|
+
version, status, created,
|
|
67057
|
+
readiness: (0, readiness_1.formatReadinessFrontmatter)(verdict),
|
|
67058
|
+
specVersion: opts.specVersion,
|
|
67059
|
+
source: opts.sourceUrl,
|
|
67060
|
+
});
|
|
67061
|
+
(0, fs_1.writeFileSync)(filePath, content, 'utf-8');
|
|
67062
|
+
};
|
|
67063
|
+
if (isLocalMode) {
|
|
67064
|
+
writeSpecFile({ canonicalId: id });
|
|
67065
|
+
}
|
|
67066
|
+
else {
|
|
67067
|
+
const result = await (0, push_spec_1.pushSpec)({
|
|
67068
|
+
client: requireCloudClient(),
|
|
67069
|
+
id,
|
|
67070
|
+
existingSpecVersion: existing?.specVersion,
|
|
67071
|
+
payload: {
|
|
67072
|
+
title: spec.title,
|
|
67073
|
+
objective: spec.objective,
|
|
67074
|
+
...(spec.currentState !== undefined ? { currentState: spec.currentState } : {}),
|
|
67075
|
+
outcomes: spec.outcomes,
|
|
67076
|
+
...(spec.constraints ? { constraints: spec.constraints } : {}),
|
|
67077
|
+
...(spec.healthMetrics ? { healthMetrics: spec.healthMetrics } : {}),
|
|
67078
|
+
...(spec.edgeCases ? { edgeCases: spec.edgeCases } : {}),
|
|
67079
|
+
...(spec.verification ? { verification: spec.verification } : {}),
|
|
67080
|
+
...(spec.scope ? { scope: spec.scope } : {}),
|
|
67081
|
+
...(spec.productId ? { productId: spec.productId } : {}),
|
|
67082
|
+
},
|
|
67083
|
+
decisions: spec.decisions,
|
|
67084
|
+
implementationContext: spec.implementationContext,
|
|
67085
|
+
onIdentitySettled: writeSpecFile,
|
|
67086
|
+
});
|
|
67087
|
+
if (!result.ok) {
|
|
67088
|
+
return {
|
|
67089
|
+
content: [{
|
|
67090
|
+
type: 'text',
|
|
67091
|
+
text: [
|
|
67092
|
+
`✗ Not saved. The workspace refused the spec, so ${filePath} was left alone.`,
|
|
67093
|
+
'',
|
|
67094
|
+
result.error,
|
|
67095
|
+
...(result.code === 'PRODUCT_REQUIRED' && result.products?.length
|
|
67096
|
+
? [
|
|
67097
|
+
'',
|
|
67098
|
+
'Ask the user which product this belongs to, then call intent_save again with productId:',
|
|
67099
|
+
...result.products.map(p => ` - ${p.id} ${p.name}${p.isExample ? ' (example product)' : ''}`),
|
|
67100
|
+
'Do not guess: the product decides which repository merge verification applies to.',
|
|
67101
|
+
]
|
|
67102
|
+
: []),
|
|
67103
|
+
'',
|
|
67104
|
+
'Writing the file anyway would leave it disagreeing with the record, invisibly.',
|
|
67105
|
+
].join('\n'),
|
|
67106
|
+
}],
|
|
67107
|
+
isError: true,
|
|
67108
|
+
};
|
|
67109
|
+
}
|
|
67110
|
+
canonicalId = result.canonicalId;
|
|
67111
|
+
specVersion = result.specVersion;
|
|
67112
|
+
sourceUrl = result.sourceUrl;
|
|
67113
|
+
didNotTravel = result.didNotTravel;
|
|
67114
|
+
}
|
|
66510
67115
|
const action = decision.action === 'update' ? `Updated intent spec (v${version})` : 'Saved intent spec';
|
|
66511
67116
|
const readinessNote = verdict.ready
|
|
66512
67117
|
? ''
|
|
@@ -66514,7 +67119,18 @@ function startMcpServer() {
|
|
|
66514
67119
|
return {
|
|
66515
67120
|
content: [{
|
|
66516
67121
|
type: 'text',
|
|
66517
|
-
text:
|
|
67122
|
+
text: [
|
|
67123
|
+
`✓ ${action} at ${filePath}`,
|
|
67124
|
+
` id: ${canonicalId} · status: ${status} · readiness: ${(0, readiness_1.formatReadinessFrontmatter)(verdict)}`,
|
|
67125
|
+
...(sourceUrl ? [` synced to ${sourceUrl}`] : []),
|
|
67126
|
+
...(didNotTravel.length
|
|
67127
|
+
? ['', 'Saved, but these did NOT reach the workspace:', ...didNotTravel.map(x => ` - ${x}`)]
|
|
67128
|
+
: []),
|
|
67129
|
+
readinessNote,
|
|
67130
|
+
...(isLocalMode
|
|
67131
|
+
? ['', 'To connect this to Pathmode for dependency tracking and team collaboration, visit pathmode.io']
|
|
67132
|
+
: []),
|
|
67133
|
+
].join('\n'),
|
|
66518
67134
|
}],
|
|
66519
67135
|
};
|
|
66520
67136
|
});
|
|
@@ -66523,8 +67139,9 @@ function startMcpServer() {
|
|
|
66523
67139
|
spec: zod_1.z.object(intentSpecSchema),
|
|
66524
67140
|
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'),
|
|
66525
67141
|
}, async ({ format, spec, path }) => {
|
|
67142
|
+
const fields = toIntentFields(spec);
|
|
66526
67143
|
if (format === 'cursorrules') {
|
|
66527
|
-
const content = (0, intent_compiler_1.formatCursorRules)(
|
|
67144
|
+
const content = (0, intent_compiler_1.formatCursorRules)(fields);
|
|
66528
67145
|
const filePath = resolveWithinProject(path || '.cursorrules');
|
|
66529
67146
|
(0, fs_1.writeFileSync)(filePath, content, 'utf-8');
|
|
66530
67147
|
return {
|
|
@@ -66535,7 +67152,7 @@ function startMcpServer() {
|
|
|
66535
67152
|
};
|
|
66536
67153
|
}
|
|
66537
67154
|
else if (format === 'outcome-rubric') {
|
|
66538
|
-
const content = (0, intent_compiler_1.formatOutcomeRubric)(
|
|
67155
|
+
const content = (0, intent_compiler_1.formatOutcomeRubric)(fields);
|
|
66539
67156
|
const filePath = resolveWithinProject(path || 'outcome-rubric.md');
|
|
66540
67157
|
(0, fs_1.writeFileSync)(filePath, content, 'utf-8');
|
|
66541
67158
|
return {
|
|
@@ -66546,7 +67163,7 @@ function startMcpServer() {
|
|
|
66546
67163
|
};
|
|
66547
67164
|
}
|
|
66548
67165
|
else {
|
|
66549
|
-
const section = (0, intent_compiler_1.formatClaudeMdSection)(
|
|
67166
|
+
const section = (0, intent_compiler_1.formatClaudeMdSection)(fields);
|
|
66550
67167
|
const defaultFile = format === 'agents-md' ? 'AGENTS.md' : 'CLAUDE.md';
|
|
66551
67168
|
const filePath = resolveWithinProject(path || defaultFile);
|
|
66552
67169
|
let existing = '';
|