fraim 2.0.298 → 2.0.299

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.
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.getLayerBackend = getLayerBackend;
7
+ exports.isLayerMisconfigured = isLayerMisconfigured;
7
8
  exports.setLayerBackend = setLayerBackend;
8
9
  exports.readUserFraimConfig = readUserFraimConfig;
9
10
  exports.writeUserFraimConfig = writeUserFraimConfig;
@@ -29,6 +30,25 @@ function getLayerBackend(layer) {
29
30
  return getManagerStorageConfig();
30
31
  return null;
31
32
  }
33
+ /**
34
+ * Returns true when the layer has a config entry whose backend is recognized
35
+ * but the required pointer field is missing (git with no gitUrl, local-folder
36
+ * with no localPath). Callers use this to distinguish "not configured" from
37
+ * "misconfigured" so the UI can surface a warning instead of silently falling
38
+ * back to single-machine. (issue #1465, R3)
39
+ */
40
+ function isLayerMisconfigured(layer) {
41
+ const raw = layer === 'org'
42
+ ? readUserFraimConfig().organization
43
+ : readUserFraimConfig().managerStorage;
44
+ if (!raw || typeof raw !== 'object')
45
+ return false;
46
+ if (raw.backend === 'git' && !raw.gitUrl)
47
+ return true;
48
+ if (raw.backend === 'local-folder' && !raw.localPath)
49
+ return true;
50
+ return false;
51
+ }
32
52
  /** Write the backend config for a named layer (issue #834). */
33
53
  function setLayerBackend(layer, config) {
34
54
  if (layer === 'org') {
@@ -113,11 +113,20 @@ exports.PERSONA_CAPABILITY_BUNDLES = {
113
113
  defaultHireMode: 'job',
114
114
  lockCopy: 'Hire SEChar to unlock security setup, review, and findings-command work for this request.'
115
115
  },
116
+ travis: {
117
+ personaKey: 'travis',
118
+ bundleId: 'persona-travis-core',
119
+ catalogMetadata: buildCatalogMetadata('travis', ['travel-planning', 'travel-disruption-rebooking', 'travel-fare-watch']),
120
+ protectedJobs: ['travel-planning', 'travel-disruption-rebooking', 'travel-fare-watch'],
121
+ protectedAliases: ['travel-agent', 'travel', 'trip-planning', 'rebooking', 'fare-watch', 'flight-watch'],
122
+ defaultHireMode: 'job',
123
+ lockCopy: 'Hire TRAVis to unlock trip planning, fare watching, travel disruption handling, and rebooking support for this request.'
124
+ },
116
125
  ashley: {
117
126
  personaKey: 'ashley',
118
127
  bundleId: 'persona-ashley-core',
119
128
  catalogMetadata: buildCatalogMetadata('ashley', ['chief-of-staff-briefing', 'executive-assistant', 'analyze-transcript']),
120
- protectedJobs: ['chief-of-staff-briefing', 'calendar-triage', 'meeting-preparation', 'executive-assistant', 'analyze-transcript', 'travel-planning', 'travel-disruption-rebooking'],
129
+ protectedJobs: ['chief-of-staff-briefing', 'calendar-triage', 'meeting-preparation', 'executive-assistant', 'analyze-transcript'],
121
130
  protectedAliases: ['executive-assistant', 'operations-assistant'],
122
131
  defaultHireMode: 'job',
123
132
  lockCopy: 'Hire AshLey to unlock executive-assistant work for this request.'
@@ -199,6 +199,15 @@ exports.PERSONA_HIRE_CATALOG = {
199
199
  jobPriceCents: 890,
200
200
  fulltimePriceCents: 4990,
201
201
  },
202
+ travis: {
203
+ displayName: 'TRAVis',
204
+ role: 'Travel Agent',
205
+ emoji: '\u{1F9ED}',
206
+ gradient: 'linear-gradient(135deg, #0f766e 0%, #0ea5e9 100%)',
207
+ blurb: 'Plans trips, watches fares, handles travel disruptions, and helps with rebooking options while keeping provider limits and booking boundaries clear.',
208
+ jobPriceCents: 890,
209
+ fulltimePriceCents: 4990,
210
+ },
202
211
  ashley: {
203
212
  displayName: 'AshLey',
204
213
  role: 'Executive Assistant',
@@ -251,6 +260,7 @@ exports.PERSONA_AVATAR_CATALOG = {
251
260
  ricardo: { seed: 'RICARDO-recruiter', bg: 'ede9fe', style: 'notionists' },
252
261
  hari: { seed: 'HARI-hr', bg: 'ccfbf1', style: 'notionists' },
253
262
  careena: { seed: 'CAREENA-career-coach', bg: 'e0f2fe', style: 'notionists' },
263
+ travis: { seed: 'TRAVIS-travel-agent', bg: 'ccfbf1', style: 'notionists' },
254
264
  sade: { seed: 'SADE-salesforce', bg: 'bae6fd', style: 'notionists' },
255
265
  sam: { seed: 'SAM-sales-account', bg: 'd1fae5', style: 'notionists' },
256
266
  casey: { seed: 'CASEY-cx', bg: 'a5f3fc', style: 'notionists' },
@@ -83,7 +83,7 @@ class AIMentor {
83
83
  const validOutcomes = (0, resolve_phase_edge_1.discriminantKeys)(onSuccess)
84
84
  .map((key) => `"${key}"`)
85
85
  .join(' | ');
86
- return `\n\n---\n\n## Report Back\nWhen this phase is done, call \`seekMentoring({ jobName: "${jobName}", issueNumber: "<issue_number>", currentPhase: "${phaseId}", status: "complete", findings: { issueType: "<outcome>" } })\` with one of: ${validOutcomes}.${failureNote}`;
86
+ return `\n\n---\n\n## Report Back\nWhen this phase is done, call \`seekMentoring({ jobName: "${jobName}", issueNumber: "<issue_number>", currentPhase: "${phaseId}", status: "complete", findings: { phaseOutcome: "<outcome>" } })\` with one of: ${validOutcomes}.${failureNote}`;
87
87
  }
88
88
  /** Phase-authority content injected for all phased workflows. Loaded from orchestration/phase-authority.md. */
89
89
  async getPhaseAuthorityContent() {
@@ -173,7 +173,14 @@ class AIMentor {
173
173
  nextPhaseId = (0, resolve_phase_edge_1.resolvePhaseEdge)(phaseFlow.onSuccess, (0, resolve_phase_edge_1.resolveDiscriminant)(findings, evidence));
174
174
  }
175
175
  let message = '';
176
- if (nextPhaseId) {
176
+ if (nextPhaseId === resolve_phase_edge_1.WAITING_FOR_NEXT_INVOCATION) {
177
+ message += [
178
+ '### Waiting For Next Invocation',
179
+ '',
180
+ 'Send a concise status update that this recurring assignment is waiting for its next scheduled check, then stop.'
181
+ ].join('\n');
182
+ }
183
+ else if (nextPhaseId) {
177
184
  message += `Moving to the next phase: **${nextPhaseId}**.\n\n`;
178
185
  let nextInstructions = workflow.phases.get(nextPhaseId);
179
186
  if (nextInstructions) {
@@ -96,6 +96,42 @@ function validateAddressFeedbackApproval(evidence) {
96
96
  }
97
97
  return null;
98
98
  }
99
+ function isAbsoluteHttpUrl(url) {
100
+ if (typeof url !== 'string' || !url)
101
+ return false;
102
+ try {
103
+ const parsed = new URL(url);
104
+ return parsed.protocol === 'http:' || parsed.protocol === 'https:';
105
+ }
106
+ catch {
107
+ return false;
108
+ }
109
+ }
110
+ function validateReviewArtifactEntry(artifact, index) {
111
+ const errors = [];
112
+ const label = `evidence.reviewHandoff.artifacts[${index}]`;
113
+ if (!artifact || typeof artifact !== 'object' || Array.isArray(artifact)) {
114
+ errors.push(`${label} must be an object`);
115
+ return errors;
116
+ }
117
+ const entry = artifact;
118
+ if ('kind' in entry && !('type' in entry)) {
119
+ errors.push(`${label} uses kind but must use type — use type, not kind for artifact type (e.g. "markdown", "docx", "source_file")`);
120
+ }
121
+ else if (!entry.type || typeof entry.type !== 'string' || !entry.type.trim()) {
122
+ errors.push(`${label}.type must be a non-empty string (e.g. "markdown", "docx", "source_file")`);
123
+ }
124
+ if (!entry.label || typeof entry.label !== 'string' || !entry.label.trim()) {
125
+ errors.push(`${label}.label must be a non-empty string`);
126
+ }
127
+ if ('url' in entry) {
128
+ errors.push(`${label} must not include url — artifact_set artifacts are local files; use path`);
129
+ }
130
+ if (!entry.path || typeof entry.path !== 'string' || !entry.path.trim()) {
131
+ errors.push(`${label}.path must be a non-empty local path`);
132
+ }
133
+ return errors;
134
+ }
99
135
  /**
100
136
  * Validates evidence.reviewHandoff. Returns null if valid, or an array of
101
137
  * human-readable error strings describing what is wrong.
@@ -103,9 +139,16 @@ function validateAddressFeedbackApproval(evidence) {
103
139
  * Required minimum shape:
104
140
  * {
105
141
  * reviewRequired: boolean,
106
- * reviewTarget: object | null,
107
142
  * artifacts: array,
143
+ * // when reviewRequired is true:
144
+ * reviewTarget: { type: "pull_request", label: string, url: string } | { type: "artifact_set", label: string },
145
+ * reviewActions: [{ kind: "approve", ... }, { kind: "request_changes", ... }],
108
146
  * }
147
+ *
148
+ * Hub normalizers (server.ts + script.js) both read reviewTarget.type, not .kind.
149
+ * Using reviewTarget.kind produces a payload that passes validation but is silently
150
+ * un-renderable. This validator enforces the renderable contract so malformed payloads
151
+ * are rejected before phase advancement.
109
152
  */
110
153
  function validateReviewHandoff(value) {
111
154
  if (value === undefined || value === null) {
@@ -119,7 +162,42 @@ function validateReviewHandoff(value) {
119
162
  if (typeof obj.reviewRequired !== 'boolean') {
120
163
  errors.push(`evidence.reviewHandoff.reviewRequired must be a boolean (got ${obj.reviewRequired === undefined ? 'missing' : typeof obj.reviewRequired})`);
121
164
  }
165
+ // artifacts must always be present and be an array
166
+ if (!('artifacts' in obj) || !Array.isArray(obj.artifacts)) {
167
+ errors.push('evidence.reviewHandoff.artifacts must be an array');
168
+ }
122
169
  if (obj.reviewRequired === true) {
170
+ const target = obj.reviewTarget;
171
+ if (target === undefined || target === null) {
172
+ errors.push('evidence.reviewHandoff.reviewTarget must be a non-null object with type equal to "pull_request" or "artifact_set" when reviewRequired is true');
173
+ }
174
+ else if (typeof target !== 'object' || Array.isArray(target)) {
175
+ errors.push('evidence.reviewHandoff.reviewTarget must be an object with type equal to "pull_request" or "artifact_set"');
176
+ }
177
+ else {
178
+ const t = target;
179
+ if ('kind' in t && !('type' in t)) {
180
+ errors.push(`evidence.reviewHandoff.reviewTarget uses kind but must use type — use type, not kind (e.g. { type: "pull_request", label: "...", url: "https://..." })`);
181
+ }
182
+ else if (t.type !== 'pull_request' && t.type !== 'artifact_set') {
183
+ errors.push(`evidence.reviewHandoff.reviewTarget.type must be "pull_request" or "artifact_set" (got ${t.type === undefined ? 'missing' : JSON.stringify(t.type)})`);
184
+ }
185
+ else if (t.type === 'pull_request') {
186
+ if (!isAbsoluteHttpUrl(t.url)) {
187
+ errors.push(`evidence.reviewHandoff.reviewTarget.url must be an absolute http or https URL for a pull_request target (got ${t.url === undefined ? 'missing' : JSON.stringify(t.url)})`);
188
+ }
189
+ if (Array.isArray(obj.artifacts) && obj.artifacts.length > 0) {
190
+ errors.push('evidence.reviewHandoff.artifacts must be empty for a pull_request target — the PR is the complete review surface; set artifacts: []');
191
+ }
192
+ }
193
+ else if (t.type === 'artifact_set') {
194
+ if (Array.isArray(obj.artifacts)) {
195
+ obj.artifacts.forEach((art, i) => {
196
+ errors.push(...validateReviewArtifactEntry(art, i));
197
+ });
198
+ }
199
+ }
200
+ }
123
201
  if (!Array.isArray(obj.reviewActions) || obj.reviewActions.length === 0) {
124
202
  errors.push('evidence.reviewHandoff.reviewActions must be a non-empty array when reviewRequired is true');
125
203
  }
@@ -135,6 +213,12 @@ function validateReviewHandoff(value) {
135
213
  }
136
214
  }
137
215
  }
216
+ else if (obj.reviewRequired === false && Array.isArray(obj.artifacts)) {
217
+ // For reviewRequired:false, validate any supplied artifact entries (no approval semantics required)
218
+ obj.artifacts.forEach((art, i) => {
219
+ errors.push(...validateReviewArtifactEntry(art, i));
220
+ });
221
+ }
138
222
  return errors.length > 0 ? errors : null;
139
223
  }
140
224
  /**
@@ -303,8 +387,12 @@ const REVIEW_HANDOFF_SCHEMA = `\`\`\`javascript
303
387
  evidence: {
304
388
  reviewHandoff: {
305
389
  reviewRequired: true,
306
- reviewTarget: { kind: "pull_request", url: "<PR URL>" } | { kind: "artifact_set", files: ["<path>"] } | null,
307
- artifacts: [{ label: "<label>", path: "<path>", kind: "<kind>" }],
390
+ // Pull request review (PR is the complete review surface -- set artifacts: []):
391
+ reviewTarget: { type: "pull_request", label: "Pull request", url: "https://github.com/org/repo/pull/N" },
392
+ artifacts: [],
393
+ // -- OR -- artifact set review (local generated files):
394
+ // reviewTarget: { type: "artifact_set", label: "Review bundle" },
395
+ // artifacts: [{ type: "markdown", label: "<label>", path: "<local-path>" }],
308
396
  summary: "<optional summary>",
309
397
  reviewActions: [
310
398
  { kind: "approve", label: "Approve" },
@@ -312,7 +400,8 @@ evidence: {
312
400
  ]
313
401
  }
314
402
  }
315
- \`\`\``;
403
+ \`\`\`
404
+ Note: reviewTarget uses "type" (not "kind"). Action entries use "kind" -- these are different fields.`;
316
405
  const NEXT_JOB_RECOMMENDATIONS_SCHEMA = `\`\`\`javascript
317
406
  evidence: {
318
407
  nextJobRecommendations: [
@@ -15,13 +15,15 @@
15
15
  * change inert for every job that authors no map.
16
16
  */
17
17
  Object.defineProperty(exports, "__esModule", { value: true });
18
- exports.FEEDBACK_PHASE_ID = void 0;
18
+ exports.FEEDBACK_PHASE_ID = exports.WAITING_FOR_NEXT_INVOCATION = void 0;
19
19
  exports.resolvePhaseEdge = resolvePhaseEdge;
20
20
  exports.resolveDiscriminant = resolveDiscriminant;
21
21
  exports.discriminantKeys = discriminantKeys;
22
22
  exports.derivePredecessorPhase = derivePredecessorPhase;
23
23
  /** The default discriminant, and the mandatory key on every authored map. */
24
24
  const DEFAULT_DISCRIMINANT = 'default';
25
+ /** Reserved transition target that parks a recurring job until the scheduler invokes it again. */
26
+ exports.WAITING_FOR_NEXT_INVOCATION = 'waiting-for-next-invocation';
25
27
  /**
26
28
  * Resolve an edge to the next phase id.
27
29
  *
@@ -1131,11 +1131,15 @@ function resolveTeamContextFile(workspaceRoot, key) {
1131
1131
  if (key === 'org' || key === 'orgRules') {
1132
1132
  const cachePath = (0, path_1.join)((0, pack_home_1.resolvePackHome)('org').contentRoot, relativePath);
1133
1133
  if ((0, fs_1.existsSync)(cachePath)) {
1134
+ const fraimDir = (0, project_fraim_paths_1.getUserFraimDirPath)();
1135
+ const orgDisplayPath = cachePath.startsWith(fraimDir)
1136
+ ? (0, project_fraim_paths_1.getUserFraimDisplayPath)(cachePath.slice(fraimDir.length).replace(/\\/g, '/').replace(/^\//, ''))
1137
+ : cachePath.replace(/\\/g, '/');
1134
1138
  return {
1135
1139
  present: true,
1136
1140
  readPath: cachePath,
1137
1141
  writePath: '',
1138
- displayPath: (0, project_fraim_paths_1.getUserFraimDisplayPath)(`org/${relativePath}`),
1142
+ displayPath: orgDisplayPath,
1139
1143
  scope,
1140
1144
  managedByOrgSync: true
1141
1145
  };
@@ -1146,16 +1150,16 @@ function resolveTeamContextFile(workspaceRoot, key) {
1146
1150
  const managerCachePath = (0, path_1.join)(managerHome.contentRoot, relativePath);
1147
1151
  if ((0, fs_1.existsSync)(managerCachePath)) {
1148
1152
  const fraimDir = (0, project_fraim_paths_1.getUserFraimDirPath)();
1149
- const relToFraim = managerCachePath.startsWith(fraimDir)
1150
- ? managerCachePath.slice(fraimDir.length).replace(/\\/g, '/').replace(/^\//, '')
1151
- : `personalized-employee/${relativePath}`;
1153
+ const mgrDisplayPath = managerCachePath.startsWith(fraimDir)
1154
+ ? (0, project_fraim_paths_1.getUserFraimDisplayPath)(managerCachePath.slice(fraimDir.length).replace(/\\/g, '/').replace(/^\//, ''))
1155
+ : managerCachePath.replace(/\\/g, '/');
1152
1156
  return {
1153
1157
  present: true,
1154
1158
  readPath: managerCachePath,
1155
- writePath: '',
1156
- displayPath: (0, project_fraim_paths_1.getUserFraimDisplayPath)(relToFraim),
1159
+ writePath: managerHome.requiresPublish ? '' : managerCachePath,
1160
+ displayPath: mgrDisplayPath,
1157
1161
  scope,
1158
- managedByManagerSync: true
1162
+ ...(managerHome.requiresPublish ? { managedByManagerSync: true } : {})
1159
1163
  };
1160
1164
  }
1161
1165
  }
@@ -25,15 +25,22 @@ const learning_context_builder_1 = require("./learning-context-builder");
25
25
  var learning_firing_parser_2 = require("./learning-firing-parser");
26
26
  Object.defineProperty(exports, "parseFiringSection", { enumerable: true, get: function () { return learning_firing_parser_2.parseFiringSection; } });
27
27
  Object.defineProperty(exports, "extractLegacyTitle", { enumerable: true, get: function () { return learning_firing_parser_2.extractLegacyTitle; } });
28
+ const ENTRY_INDEX_READS = [
29
+ { scope: 'manager', level: 'machine' },
30
+ { scope: 'manager', level: 'project' },
31
+ { scope: 'org', level: 'machine' },
32
+ { scope: 'reverse', level: 'machine' },
33
+ { scope: 'reverse', level: 'project' },
34
+ ];
28
35
  function buildEntryIndex(workspaceRoot, userId) {
29
36
  const byId = new Map();
30
37
  const byTitle = new Map();
31
- // Every scope a firing could name: the manager's own entries, the org entries
32
- // the agent also reads, and the manager-coaching family.
33
- for (const scope of ['manager', 'org', 'reverse']) {
38
+ // Keep this list in parity with the preserved-entry levels that the offer
39
+ // path can surface. Storage resolution stays inside readPreservedLearnings.
40
+ for (const { scope, level } of ENTRY_INDEX_READS) {
34
41
  let entries = [];
35
42
  try {
36
- entries = (0, learning_context_builder_1.readPreservedLearnings)(workspaceRoot, userId, scope);
43
+ entries = (0, learning_context_builder_1.readPreservedLearnings)(workspaceRoot, userId, scope, level);
37
44
  }
38
45
  catch {
39
46
  continue;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fraim",
3
- "version": "2.0.298",
3
+ "version": "2.0.299",
4
4
  "description": "FRAIM core CLI and MCP package.",
5
5
  "main": "index.js",
6
6
  "bin": {