@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/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
- throw new Error(`API error (${response.status}): ${body.error || response.statusText}`);
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 || `intent_${Date.now()}`,
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
- for (const t of spec.implementationContext?.verificationSuggestions ?? [])
34618
- if (t?.trim())
34619
- checks.push(`[suggested] ${t}`);
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
- /***/ 4239:
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.13.1","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"}}');
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 stdio_js_1 = __nccwpck_require__(4239);
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. 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.',
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, objective, and productId. Returns the created intent with its ID. Use list_intents first to see existing intents and avoid duplicates.',
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. Use get_workspace to find product IDs.'),
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\n5. If verification passes, use update_intent_status to mark it as "shipped"\n6. Use log_implementation_note to document key technical decisions`,
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. 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.', {
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: () => `intent_${Date.now()}`,
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 verdict = (0, readiness_1.computeReadinessVerdict)(spec);
66508
- const content = (0, intent_compiler_1.formatIntentMd)({ ...spec, id }, { version, status, created, readiness: (0, readiness_1.formatReadinessFrontmatter)(verdict) });
66509
- (0, fs_1.writeFileSync)(filePath, content, 'utf-8');
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: `✓ ${action} at ${filePath}\n id: ${id} · status: ${status} · readiness: ${(0, readiness_1.formatReadinessFrontmatter)(verdict)}${readinessNote}\n\nTo connect this to Pathmode for dependency tracking and team collaboration, visit pathmode.io`,
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)(spec);
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)(spec);
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)(spec);
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 = '';