fraim-framework 2.0.123 → 2.0.126
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/bin/fraim.js +1 -1
- package/dist/src/cli/setup/ide-invocation-surfaces.js +56 -17
- package/dist/src/cli/utils/agent-adapters.js +2 -2
- package/dist/src/cli/utils/project-bootstrap.js +4 -3
- package/dist/src/core/quality-evidence.js +141 -41
- package/dist/src/core/utils/git-utils.js +21 -4
- package/dist/src/core/utils/job-aliases.js +47 -0
- package/dist/src/core/utils/workflow-parser.js +3 -5
- package/dist/src/local-mcp-server/agent-token-prices.js +114 -0
- package/dist/src/local-mcp-server/codex-token-adapter.js +232 -0
- package/dist/src/local-mcp-server/learning-context-builder.js +21 -8
- package/dist/src/local-mcp-server/stdio-server.js +39 -9
- package/dist/src/local-mcp-server/token-adapter-registry.js +64 -0
- package/dist/src/local-mcp-server/usage-collector.js +25 -0
- package/index.js +83 -83
- package/package.json +5 -1
- package/dist/src/cli/services/device-flow-service.js +0 -83
- package/dist/src/local-mcp-server/prometheus-scraper.js +0 -152
package/bin/fraim.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.FRAIM_INVOCATION_BODY = exports.CURSOR_MDC_FRONTMATTER = exports.FRAIM_LAUNCH_PHRASE = void 0;
|
|
3
|
+
exports.FRAIM_INVOCATION_BODY = exports.FRAIM_DEFERRED_TOOL_PRELOAD = exports.CURSOR_MDC_FRONTMATTER = exports.FRAIM_LAUNCH_PHRASE = void 0;
|
|
4
|
+
exports.buildFraimInvocationBody = buildFraimInvocationBody;
|
|
4
5
|
exports.buildClaudeSkillContent = buildClaudeSkillContent;
|
|
5
6
|
exports.buildClaudeCommandShimContent = buildClaudeCommandShimContent;
|
|
6
7
|
exports.buildClaudeSlashCommandContent = buildClaudeSlashCommandContent;
|
|
@@ -14,9 +15,45 @@ exports.CURSOR_MDC_FRONTMATTER = `---
|
|
|
14
15
|
description: FRAIM discovery and execution contract
|
|
15
16
|
alwaysApply: true
|
|
16
17
|
---`;
|
|
17
|
-
exports.
|
|
18
|
+
exports.FRAIM_DEFERRED_TOOL_PRELOAD = [
|
|
19
|
+
'fraim_connect',
|
|
20
|
+
'list_fraim_jobs',
|
|
21
|
+
'get_fraim_job',
|
|
22
|
+
'get_fraim_file',
|
|
23
|
+
'seekMentoring'
|
|
24
|
+
];
|
|
25
|
+
function buildDeferredToolBootstrapSection(profile) {
|
|
26
|
+
if (profile === 'none') {
|
|
27
|
+
return '';
|
|
28
|
+
}
|
|
29
|
+
const preloadList = exports.FRAIM_DEFERRED_TOOL_PRELOAD.map((toolName) => `\`${toolName}\``).join(', ');
|
|
30
|
+
switch (profile) {
|
|
31
|
+
case 'claude-toolsearch':
|
|
32
|
+
return `0. **Preload deferred FRAIM tools when needed**:
|
|
33
|
+
- If FRAIM MCP tools are unavailable because this host lazily loads deferred tool schemas, call \`ToolSearch\` once to load ${preloadList}.
|
|
34
|
+
- Do the preload as one batched discovery step, not one search per tool.
|
|
35
|
+
|
|
36
|
+
`;
|
|
37
|
+
case 'codex-tool-search':
|
|
38
|
+
return `0. **Preload deferred FRAIM tools when needed**:
|
|
39
|
+
- If FRAIM MCP tools are unavailable because this host lazily loads deferred tool schemas, call \`tool_search\` once to load ${preloadList}.
|
|
40
|
+
- Do the preload as one batched discovery step, not one search per tool.
|
|
41
|
+
|
|
42
|
+
`;
|
|
43
|
+
case 'generic-tool-discovery':
|
|
44
|
+
return `0. **Preload deferred FRAIM tools when needed**:
|
|
45
|
+
- If FRAIM MCP tools are unavailable because this host lazily loads deferred tool schemas, use the host's tool discovery surface once to load ${preloadList}.
|
|
46
|
+
- Do the preload as one batched discovery step, not one search per tool.
|
|
18
47
|
|
|
19
|
-
|
|
48
|
+
`;
|
|
49
|
+
default:
|
|
50
|
+
return '';
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function buildFraimInvocationBody(profile = 'none') {
|
|
54
|
+
return `Follow this process:
|
|
55
|
+
|
|
56
|
+
${buildDeferredToolBootstrapSection(profile)}1. **If the user did not specify a FRAIM job or topic**:
|
|
20
57
|
Call \`list_fraim_jobs()\` to discover available jobs. Present the results grouped by the categories returned by the server. For each group, list 3-5 of the most relevant jobs with a one-line description.
|
|
21
58
|
|
|
22
59
|
2. **Find the match**:
|
|
@@ -27,20 +64,22 @@ exports.FRAIM_INVOCATION_BODY = `Follow this process:
|
|
|
27
64
|
- For skills, use the content returned by \`get_fraim_file(...)\`.
|
|
28
65
|
|
|
29
66
|
4. **Execute**:
|
|
30
|
-
- For jobs, follow the phased instructions and use \`seekMentoring\` when the job requires phase transitions.
|
|
31
|
-
- For skills, apply the skill steps directly to the user's current context.
|
|
67
|
+
- For jobs, follow the phased instructions and use \`seekMentoring\` when the job requires phase transitions.
|
|
68
|
+
- For skills, apply the skill steps directly to the user's current context.
|
|
32
69
|
`;
|
|
70
|
+
}
|
|
71
|
+
exports.FRAIM_INVOCATION_BODY = buildFraimInvocationBody();
|
|
33
72
|
function buildClaudeSkillContent() {
|
|
34
|
-
return `# FRAIM
|
|
35
|
-
|
|
36
|
-
${
|
|
73
|
+
return `# FRAIM
|
|
74
|
+
|
|
75
|
+
${buildFraimInvocationBody('claude-toolsearch')}`;
|
|
37
76
|
}
|
|
38
77
|
function buildClaudeCommandShimContent() {
|
|
39
|
-
return `# FRAIM Compatibility Command
|
|
40
|
-
|
|
41
|
-
Use the FRAIM skill when Claude exposes skills directly. This compatibility command keeps \`/fraim\` working on surfaces that still discover legacy command files.
|
|
42
|
-
|
|
43
|
-
${
|
|
78
|
+
return `# FRAIM Compatibility Command
|
|
79
|
+
|
|
80
|
+
Use the FRAIM skill when Claude exposes skills directly. This compatibility command keeps \`/fraim\` working on surfaces that still discover legacy command files.
|
|
81
|
+
|
|
82
|
+
${buildFraimInvocationBody('claude-toolsearch')}`;
|
|
44
83
|
}
|
|
45
84
|
function buildClaudeSlashCommandContent() {
|
|
46
85
|
return buildClaudeCommandShimContent();
|
|
@@ -50,23 +89,23 @@ function buildCursorMentionRuleContent() {
|
|
|
50
89
|
|
|
51
90
|
# FRAIM
|
|
52
91
|
|
|
53
|
-
${
|
|
92
|
+
${buildFraimInvocationBody('generic-tool-discovery')}
|
|
54
93
|
`;
|
|
55
94
|
}
|
|
56
95
|
function buildCodexSkillContent() {
|
|
57
96
|
return `# FRAIM
|
|
58
97
|
|
|
59
|
-
${
|
|
98
|
+
${buildFraimInvocationBody('codex-tool-search')}`;
|
|
60
99
|
}
|
|
61
100
|
function buildWindsurfCommandContent() {
|
|
62
101
|
return `# FRAIM
|
|
63
102
|
|
|
64
|
-
${
|
|
103
|
+
${buildFraimInvocationBody('generic-tool-discovery')}`;
|
|
65
104
|
}
|
|
66
105
|
function buildKiroCommandContent() {
|
|
67
106
|
return `# FRAIM
|
|
68
107
|
|
|
69
|
-
${
|
|
108
|
+
${buildFraimInvocationBody('generic-tool-discovery')}`;
|
|
70
109
|
}
|
|
71
110
|
function describeInvocationSurface(ideName, invocationProfile) {
|
|
72
111
|
switch (invocationProfile) {
|
|
@@ -74,7 +74,7 @@ This repository uses FRAIM.
|
|
|
74
74
|
const cursorManagedBody = buildManagedSection(`
|
|
75
75
|
# FRAIM
|
|
76
76
|
|
|
77
|
-
${ide_invocation_surfaces_1.
|
|
77
|
+
${(0, ide_invocation_surfaces_1.buildFraimInvocationBody)('generic-tool-discovery')}
|
|
78
78
|
`);
|
|
79
79
|
const copilotBody = buildManagedSection(`
|
|
80
80
|
## FRAIM
|
|
@@ -101,7 +101,7 @@ Use the stubs here to discover which FRAIM job, skill, or rule is relevant, then
|
|
|
101
101
|
`;
|
|
102
102
|
const vscodePrompt = `# FRAIM
|
|
103
103
|
|
|
104
|
-
${ide_invocation_surfaces_1.
|
|
104
|
+
${(0, ide_invocation_surfaces_1.buildFraimInvocationBody)('generic-tool-discovery')}`;
|
|
105
105
|
return [
|
|
106
106
|
{ path: 'AGENTS.md', content: markdownBody },
|
|
107
107
|
{ path: 'CLAUDE.md', content: markdownBody },
|
|
@@ -8,6 +8,7 @@ exports.recordPathStatus = recordPathStatus;
|
|
|
8
8
|
exports.buildInitProjectSummary = buildInitProjectSummary;
|
|
9
9
|
exports.printInitProjectSummary = printInitProjectSummary;
|
|
10
10
|
const chalk_1 = __importDefault(require("chalk"));
|
|
11
|
+
const ONBOARDING_VIDEO_PLAYLIST_URL = 'https://fraimworks.ai/resources.html#videos';
|
|
11
12
|
function formatModeLabel(mode) {
|
|
12
13
|
switch (mode) {
|
|
13
14
|
case 'conversational':
|
|
@@ -21,11 +22,11 @@ function formatModeLabel(mode) {
|
|
|
21
22
|
function getModeSpecificNextStep(mode) {
|
|
22
23
|
switch (mode) {
|
|
23
24
|
case 'conversational':
|
|
24
|
-
return
|
|
25
|
+
return `The agent will focus on project context, validation commands, and durable repo rules. For a walkthrough, watch the onboarding videos at ${ONBOARDING_VIDEO_PLAYLIST_URL}.`;
|
|
25
26
|
case 'split':
|
|
26
|
-
return
|
|
27
|
+
return `The agent will confirm the code-host and issue-tracker split, then ask only for the missing project details. For a walkthrough, watch the onboarding videos at ${ONBOARDING_VIDEO_PLAYLIST_URL}.`;
|
|
27
28
|
default:
|
|
28
|
-
return
|
|
29
|
+
return `The agent will review the detected repo setup, then ask only for the highest-value missing project details. For a walkthrough, watch the onboarding videos at ${ONBOARDING_VIDEO_PLAYLIST_URL}.`;
|
|
29
30
|
}
|
|
30
31
|
}
|
|
31
32
|
function createInitProjectResult(projectName, mode) {
|
|
@@ -20,31 +20,30 @@
|
|
|
20
20
|
* seekMentoring handler (e.g., when the local AIMentor errors).
|
|
21
21
|
*/
|
|
22
22
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
23
|
-
exports.REQUIRED_QUALITY_FIELDS = exports.ALL_STAGE_CATEGORIES = exports.STAGE_DISPLAY_NAMES = exports.STAGE_CATEGORY_MAP = exports.QUALITY_PRODUCING_JOBS = exports.QUALITY_REGISTRY = void 0;
|
|
23
|
+
exports.REQUIRED_QUALITY_FIELDS = exports.ALL_STAGE_CATEGORIES = exports.STAGE_DISPLAY_NAMES = exports.STAGE_CATEGORY_MAP = exports.QUALITY_GATE_JOBS = exports.QUALITY_SCORE_JOBS = exports.QUALITY_PRODUCING_JOBS = exports.QUALITY_REGISTRY = void 0;
|
|
24
24
|
exports.getRequiredFieldsForJob = getRequiredFieldsForJob;
|
|
25
25
|
exports.validateQualityEvidence = validateQualityEvidence;
|
|
26
26
|
exports.buildQualityRejectionMessage = buildQualityRejectionMessage;
|
|
27
|
-
/**
|
|
28
|
-
* Central registry of FRAIM jobs that interact with the quality telemetry system.
|
|
29
|
-
* This is the SINGLE SOURCE OF TRUTH for:
|
|
30
|
-
* 1. Stage mapping (where it appears on the dashboard).
|
|
31
|
-
* 2. Enforcement (whether it MUST emit a quality score to complete).
|
|
32
|
-
*/
|
|
33
27
|
exports.QUALITY_REGISTRY = {
|
|
34
28
|
// Customer Development
|
|
35
|
-
|
|
36
|
-
|
|
29
|
+
// review-customer-development is the sole quality emitter for this stage.
|
|
30
|
+
// process-interview-notes is retained here for stage mapping only; it does
|
|
31
|
+
// not emit (enforced: false).
|
|
32
|
+
'process-interview-notes': { stage: 'customer-development', enforced: false },
|
|
33
|
+
'review-customer-development': { stage: 'customer-development', enforced: true, telemetryKind: 'score' },
|
|
34
|
+
'triage-customer-needs': { stage: 'customer-development', enforced: true, telemetryKind: 'gate' },
|
|
37
35
|
'interview-preparation': { stage: 'customer-discovery', enforced: false },
|
|
38
36
|
// Business Strategy
|
|
39
|
-
'review-business-strategy': { stage: 'business-strategy', enforced: true },
|
|
37
|
+
'review-business-strategy': { stage: 'business-strategy', enforced: true, telemetryKind: 'score' },
|
|
40
38
|
'business-plan-creation': { stage: 'business-strategy', enforced: false },
|
|
41
|
-
'branding-quality-audit': { stage: 'branding', enforced: true },
|
|
39
|
+
'branding-quality-audit': { stage: 'branding', enforced: true, telemetryKind: 'score' },
|
|
42
40
|
// Product Quality
|
|
43
|
-
'code-quality-assessment': { stage: 'product-quality', enforced: true },
|
|
41
|
+
'code-quality-assessment': { stage: 'product-quality', enforced: true, telemetryKind: 'score' },
|
|
44
42
|
// Test Quality
|
|
45
|
-
'test-quality-assessment': { stage: 'test-quality', enforced: true },
|
|
43
|
+
'test-quality-assessment': { stage: 'test-quality', enforced: true, telemetryKind: 'score' },
|
|
46
44
|
// Security
|
|
47
|
-
'security-review': { stage: 'security', enforced: true },
|
|
45
|
+
'security-review': { stage: 'security', enforced: true, telemetryKind: 'score' },
|
|
46
|
+
'production-readiness-review': { stage: 'production-readiness', enforced: true, telemetryKind: 'score' },
|
|
48
47
|
// Fundraising
|
|
49
48
|
'investor-pitch-preparation': { stage: 'fundraising', enforced: false },
|
|
50
49
|
// Go-to-Market
|
|
@@ -55,6 +54,15 @@ exports.QUALITY_REGISTRY = {
|
|
|
55
54
|
* Derived enforcement list for backward compatibility.
|
|
56
55
|
*/
|
|
57
56
|
exports.QUALITY_PRODUCING_JOBS = Object.keys(exports.QUALITY_REGISTRY).filter((job) => exports.QUALITY_REGISTRY[job].enforced);
|
|
57
|
+
/**
|
|
58
|
+
* Jobs that write scored review rows used by the Quality tab scorecards.
|
|
59
|
+
* Gate-only telemetry jobs (for example `triage-customer-needs`) are excluded.
|
|
60
|
+
*/
|
|
61
|
+
exports.QUALITY_SCORE_JOBS = Object.keys(exports.QUALITY_REGISTRY).filter((job) => exports.QUALITY_REGISTRY[job].enforced && exports.QUALITY_REGISTRY[job].telemetryKind !== 'gate');
|
|
62
|
+
/**
|
|
63
|
+
* Jobs that emit gate-style telemetry without a composite score.
|
|
64
|
+
*/
|
|
65
|
+
exports.QUALITY_GATE_JOBS = Object.keys(exports.QUALITY_REGISTRY).filter((job) => exports.QUALITY_REGISTRY[job].enforced && exports.QUALITY_REGISTRY[job].telemetryKind === 'gate');
|
|
58
66
|
/**
|
|
59
67
|
* @deprecated Use QUALITY_REGISTRY for new code.
|
|
60
68
|
* Derived stage mappings for backward compatibility.
|
|
@@ -70,6 +78,7 @@ exports.STAGE_DISPLAY_NAMES = {
|
|
|
70
78
|
'product-quality': 'Product Quality',
|
|
71
79
|
'test-quality': 'Test Quality',
|
|
72
80
|
'security': 'Security',
|
|
81
|
+
'production-readiness': 'Production Readiness',
|
|
73
82
|
'fundraising': 'Fundraising',
|
|
74
83
|
'go-to-market': 'Go-to-Market',
|
|
75
84
|
};
|
|
@@ -83,6 +92,7 @@ exports.ALL_STAGE_CATEGORIES = [
|
|
|
83
92
|
'product-quality',
|
|
84
93
|
'test-quality',
|
|
85
94
|
'security',
|
|
95
|
+
'production-readiness',
|
|
86
96
|
'fundraising',
|
|
87
97
|
'go-to-market',
|
|
88
98
|
];
|
|
@@ -97,6 +107,12 @@ const CUSTOMER_DEV_REQUIRED_FIELDS = [
|
|
|
97
107
|
{ path: 'participant.authority', type: 'number' },
|
|
98
108
|
{ path: 'evidence.quoteSpecificityAvg', type: 'number' }
|
|
99
109
|
];
|
|
110
|
+
/**
|
|
111
|
+
* Gate-only customer development jobs emit a decision without a composite score.
|
|
112
|
+
*/
|
|
113
|
+
const GATE_REQUIRED_FIELDS = [
|
|
114
|
+
{ path: 'gateDecision', type: 'string' }
|
|
115
|
+
];
|
|
100
116
|
/**
|
|
101
117
|
* Required fields for all other quality-producing jobs.
|
|
102
118
|
* Only composite and coaching are universally required — sub-dimensions
|
|
@@ -105,17 +121,85 @@ const CUSTOMER_DEV_REQUIRED_FIELDS = [
|
|
|
105
121
|
const UNIVERSAL_REQUIRED_FIELDS = [
|
|
106
122
|
{ path: 'composite', type: 'number' },
|
|
107
123
|
];
|
|
124
|
+
const SCORE_DIMENSION_REQUIRED_FIELDS = [
|
|
125
|
+
{ suffix: 'score', type: 'number' },
|
|
126
|
+
{ suffix: 'rationale', type: 'string' }
|
|
127
|
+
];
|
|
128
|
+
const QUALITY_SCORE_DIMENSIONS = {
|
|
129
|
+
'review-customer-development': [
|
|
130
|
+
'icpCoherence',
|
|
131
|
+
'interviewCoverage',
|
|
132
|
+
'evidenceQuality',
|
|
133
|
+
'patternSaturation',
|
|
134
|
+
'signalToProduct'
|
|
135
|
+
],
|
|
136
|
+
'review-business-strategy': [
|
|
137
|
+
'marketEvidence',
|
|
138
|
+
'competitiveRigor',
|
|
139
|
+
'unitEconomics',
|
|
140
|
+
'strategicCoherence'
|
|
141
|
+
],
|
|
142
|
+
'branding-quality-audit': [
|
|
143
|
+
'clarity',
|
|
144
|
+
'differentiation',
|
|
145
|
+
'coherence',
|
|
146
|
+
'proof',
|
|
147
|
+
'identityExpressiveness',
|
|
148
|
+
'governanceReadiness'
|
|
149
|
+
],
|
|
150
|
+
'code-quality-assessment': [
|
|
151
|
+
'typeSafety',
|
|
152
|
+
'errorHandling',
|
|
153
|
+
'architecture',
|
|
154
|
+
'maintainability'
|
|
155
|
+
],
|
|
156
|
+
'test-quality-assessment': [
|
|
157
|
+
'coverage',
|
|
158
|
+
'testIntegrity',
|
|
159
|
+
'testDesign',
|
|
160
|
+
'reliability'
|
|
161
|
+
],
|
|
162
|
+
'security-review': [
|
|
163
|
+
'findingSeverity',
|
|
164
|
+
'remediationReadiness',
|
|
165
|
+
'coverageCompleteness'
|
|
166
|
+
],
|
|
167
|
+
'production-readiness-review': [
|
|
168
|
+
'securityPosture',
|
|
169
|
+
'availabilityResilience',
|
|
170
|
+
'backupRestore',
|
|
171
|
+
'observabilityOps',
|
|
172
|
+
'releaseSafety',
|
|
173
|
+
'governanceRunbooks'
|
|
174
|
+
]
|
|
175
|
+
};
|
|
176
|
+
function requiredDimensionFields(jobName) {
|
|
177
|
+
const dimensions = QUALITY_SCORE_DIMENSIONS[jobName] ?? [];
|
|
178
|
+
return dimensions.flatMap((dimension) => SCORE_DIMENSION_REQUIRED_FIELDS.map(({ suffix, type }) => ({
|
|
179
|
+
path: `${dimension}.${suffix}`,
|
|
180
|
+
type
|
|
181
|
+
})));
|
|
182
|
+
}
|
|
108
183
|
/**
|
|
109
184
|
* Returns the required quality fields for a given job.
|
|
110
|
-
*
|
|
111
|
-
*
|
|
185
|
+
* Gate-only jobs use the gate schema. The strict V1 interview schema is
|
|
186
|
+
* pinned to process-interview-notes for defense-in-depth on the
|
|
187
|
+
* /api/analytics/quality-score endpoint. Scored quality review jobs require
|
|
188
|
+
* their stage-specific rubric dimensions as direct `{ score, rationale }`
|
|
189
|
+
* properties so the Quality view can render those dimensions dynamically.
|
|
112
190
|
*/
|
|
113
191
|
function getRequiredFieldsForJob(jobName) {
|
|
114
192
|
const entry = exports.QUALITY_REGISTRY[jobName];
|
|
115
|
-
if (entry?.
|
|
193
|
+
if (entry?.telemetryKind === 'gate') {
|
|
194
|
+
return GATE_REQUIRED_FIELDS;
|
|
195
|
+
}
|
|
196
|
+
if (jobName === 'process-interview-notes') {
|
|
116
197
|
return CUSTOMER_DEV_REQUIRED_FIELDS;
|
|
117
198
|
}
|
|
118
|
-
return
|
|
199
|
+
return [
|
|
200
|
+
...UNIVERSAL_REQUIRED_FIELDS,
|
|
201
|
+
...requiredDimensionFields(jobName)
|
|
202
|
+
];
|
|
119
203
|
}
|
|
120
204
|
/**
|
|
121
205
|
* @deprecated Use getRequiredFieldsForJob() for stage-aware validation.
|
|
@@ -174,38 +258,54 @@ function validateQualityEvidence(quality, jobName) {
|
|
|
174
258
|
function buildQualityRejectionMessage(jobName, currentPhase, errors) {
|
|
175
259
|
const errorBullets = errors.map(e => `- ${e}`).join('\n');
|
|
176
260
|
const entry = exports.QUALITY_REGISTRY[jobName];
|
|
261
|
+
const isGateOnly = entry?.telemetryKind === 'gate';
|
|
177
262
|
const isCustomerDev = entry?.stage === 'customer-development';
|
|
178
|
-
const
|
|
263
|
+
const dimensionExamples = (QUALITY_SCORE_DIMENSIONS[jobName] ?? ['marketEvidence', 'unitEconomics'])
|
|
264
|
+
.map((dimension) => ` ${dimension}: { score: <number>, rationale: "<string>" },`)
|
|
265
|
+
.join('\n');
|
|
266
|
+
const schemaExample = isGateOnly
|
|
179
267
|
? [
|
|
180
268
|
'```javascript',
|
|
181
269
|
'evidence: {',
|
|
182
270
|
' quality: {',
|
|
183
|
-
'
|
|
184
|
-
'
|
|
185
|
-
'
|
|
186
|
-
'
|
|
187
|
-
'
|
|
188
|
-
' },',
|
|
189
|
-
' coaching: "<actionable recommendation>",',
|
|
190
|
-
' interviewee: "<name>",',
|
|
191
|
-
' company: "<company>"',
|
|
271
|
+
' gateDecision: "<pass|flag|fail>",',
|
|
272
|
+
' interviewsAnalyzed: <number>,',
|
|
273
|
+
' distinctPainPatterns: <number>,',
|
|
274
|
+
' gaps: ["<gap summary>"],',
|
|
275
|
+
' coaching: "<actionable recommendation>"',
|
|
192
276
|
' }',
|
|
193
277
|
'}',
|
|
194
278
|
'```'
|
|
195
279
|
]
|
|
196
|
-
:
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
280
|
+
: isCustomerDev
|
|
281
|
+
? [
|
|
282
|
+
'```javascript',
|
|
283
|
+
'evidence: {',
|
|
284
|
+
' quality: {',
|
|
285
|
+
' composite: <number 0-10>,',
|
|
286
|
+
' participant: { fit: <number 1-10>, urgency: <number 1-10>, authority: <number 1-10> },',
|
|
287
|
+
' evidence: {',
|
|
288
|
+
' quoteSpecificityAvg: <number 1-10>',
|
|
289
|
+
' // other fields are allowed and encouraged but not required',
|
|
290
|
+
' },',
|
|
291
|
+
' coaching: "<actionable recommendation>",',
|
|
292
|
+
' interviewee: "<name>",',
|
|
293
|
+
' company: "<company>"',
|
|
294
|
+
' }',
|
|
295
|
+
'}',
|
|
296
|
+
'```'
|
|
297
|
+
]
|
|
298
|
+
: [
|
|
299
|
+
'```javascript',
|
|
300
|
+
'evidence: {',
|
|
301
|
+
' quality: {',
|
|
302
|
+
' composite: <number 0-10>,',
|
|
303
|
+
' coaching: "<actionable recommendation>",',
|
|
304
|
+
dimensionExamples,
|
|
305
|
+
' }',
|
|
306
|
+
'}',
|
|
307
|
+
'```'
|
|
308
|
+
];
|
|
209
309
|
const importantNote = [
|
|
210
310
|
'',
|
|
211
311
|
'> [!IMPORTANT]',
|
|
@@ -7,6 +7,18 @@ exports.determineSchema = determineSchema;
|
|
|
7
7
|
exports.getDefaultBranch = getDefaultBranch;
|
|
8
8
|
exports.sanitizeRepoIdentifier = sanitizeRepoIdentifier;
|
|
9
9
|
const child_process_1 = require("child_process");
|
|
10
|
+
function extractLocalFolderLabel(rawPath) {
|
|
11
|
+
const normalized = rawPath
|
|
12
|
+
.trim()
|
|
13
|
+
.replace(/^file:\/\//i, '')
|
|
14
|
+
.replace(/\\/g, '/')
|
|
15
|
+
.replace(/\/+$/, '');
|
|
16
|
+
if (!normalized)
|
|
17
|
+
return undefined;
|
|
18
|
+
const parts = normalized.split('/').filter(Boolean);
|
|
19
|
+
const candidate = parts[parts.length - 1];
|
|
20
|
+
return candidate || undefined;
|
|
21
|
+
}
|
|
10
22
|
/**
|
|
11
23
|
* Gets a unique port based on the current git branch name (if it's an issue branch)
|
|
12
24
|
* Default to 15302 if not on an issue branch
|
|
@@ -106,13 +118,14 @@ function sanitizeRepoIdentifier(repoUrl) {
|
|
|
106
118
|
return undefined;
|
|
107
119
|
}
|
|
108
120
|
const trimmed = repoUrl.trim();
|
|
109
|
-
//
|
|
121
|
+
// Convert local paths to just the folder name for privacy-safe workspace labeling.
|
|
110
122
|
if (trimmed.startsWith('/') ||
|
|
111
123
|
trimmed.startsWith('\\') ||
|
|
112
|
-
/^[a-zA-Z]
|
|
124
|
+
/^[a-zA-Z]:[\\/]/.test(trimmed) ||
|
|
113
125
|
trimmed.startsWith('./') ||
|
|
114
|
-
trimmed.startsWith('../')
|
|
115
|
-
|
|
126
|
+
trimmed.startsWith('../') ||
|
|
127
|
+
trimmed.startsWith('file://')) {
|
|
128
|
+
return extractLocalFolderLabel(trimmed);
|
|
116
129
|
}
|
|
117
130
|
// Normalize GitHub/GitLab SSH and HTTPS URLs
|
|
118
131
|
// Patterns:
|
|
@@ -144,5 +157,9 @@ function sanitizeRepoIdentifier(repoUrl) {
|
|
|
144
157
|
catch (e) {
|
|
145
158
|
// Not a valid URL
|
|
146
159
|
}
|
|
160
|
+
// Preserve plain folder labels that are already privacy-safe and not path-like.
|
|
161
|
+
if (!trimmed.includes('/') && !trimmed.includes('\\') && !trimmed.includes('://')) {
|
|
162
|
+
return trimmed;
|
|
163
|
+
}
|
|
147
164
|
return undefined;
|
|
148
165
|
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DEPRECATED_FRAIM_JOB_NAMES = void 0;
|
|
4
|
+
exports.resolveFraimJobName = resolveFraimJobName;
|
|
5
|
+
exports.isListableFraimJob = isListableFraimJob;
|
|
6
|
+
const DEPRECATED_TO_CANONICAL_JOB_MAP = {
|
|
7
|
+
'learn-and-scale': 'upskill-employee',
|
|
8
|
+
'model-behavior': 'upskill-employee',
|
|
9
|
+
'promote-learning': 'upskill-employee',
|
|
10
|
+
'refine-jobs': 'upskill-employee',
|
|
11
|
+
'refine-skills': 'upskill-employee'
|
|
12
|
+
};
|
|
13
|
+
const DIRECT_JOB_ALIASES = {
|
|
14
|
+
'sleep on learnings': 'sleep-on-learnings'
|
|
15
|
+
};
|
|
16
|
+
exports.DEPRECATED_FRAIM_JOB_NAMES = new Set(Object.keys(DEPRECATED_TO_CANONICAL_JOB_MAP));
|
|
17
|
+
function normalizeJobLookupInput(input) {
|
|
18
|
+
return input.trim().toLowerCase().replace(/[_\s]+/g, '-');
|
|
19
|
+
}
|
|
20
|
+
function resolveFraimJobName(input) {
|
|
21
|
+
const normalizedJobName = normalizeJobLookupInput(input);
|
|
22
|
+
const directAliasTarget = DIRECT_JOB_ALIASES[input.trim().toLowerCase()];
|
|
23
|
+
if (directAliasTarget) {
|
|
24
|
+
return {
|
|
25
|
+
requestedJobName: input,
|
|
26
|
+
normalizedJobName,
|
|
27
|
+
canonicalJobName: directAliasTarget
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
const deprecatedAliasTarget = DEPRECATED_TO_CANONICAL_JOB_MAP[normalizedJobName];
|
|
31
|
+
if (deprecatedAliasTarget) {
|
|
32
|
+
return {
|
|
33
|
+
requestedJobName: input,
|
|
34
|
+
normalizedJobName,
|
|
35
|
+
canonicalJobName: deprecatedAliasTarget,
|
|
36
|
+
deprecatedAliasTarget
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
requestedJobName: input,
|
|
41
|
+
normalizedJobName,
|
|
42
|
+
canonicalJobName: normalizedJobName
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function isListableFraimJob(jobName) {
|
|
46
|
+
return !exports.DEPRECATED_FRAIM_JOB_NAMES.has(normalizeJobLookupInput(jobName));
|
|
47
|
+
}
|
|
@@ -5,15 +5,13 @@ const fs_1 = require("fs");
|
|
|
5
5
|
const path_1 = require("path");
|
|
6
6
|
class WorkflowParser {
|
|
7
7
|
static extractMetadataBlock(content) {
|
|
8
|
-
|
|
9
|
-
const frontmatterMatch = content.match(/^[\s\S]*?---\r?\n([\s\S]+?)\r?\n---/);
|
|
8
|
+
const frontmatterMatch = content.match(/^---\r?\n([\s\S]+?)\r?\n---/);
|
|
10
9
|
if (frontmatterMatch) {
|
|
11
10
|
try {
|
|
12
|
-
const startIndex = frontmatterMatch.index || 0;
|
|
13
11
|
return {
|
|
14
12
|
state: 'valid',
|
|
15
13
|
metadata: JSON.parse(frontmatterMatch[1]),
|
|
16
|
-
bodyStartIndex:
|
|
14
|
+
bodyStartIndex: frontmatterMatch[0].length
|
|
17
15
|
};
|
|
18
16
|
}
|
|
19
17
|
catch {
|
|
@@ -109,7 +107,7 @@ class WorkflowParser {
|
|
|
109
107
|
overview = contentAfterMetadata;
|
|
110
108
|
}
|
|
111
109
|
const phases = new Map();
|
|
112
|
-
const phaseSections = restOfContent.split(/^##\s+Phase:\s
|
|
110
|
+
const phaseSections = restOfContent.split(/^##\s+Phase:\s+/m);
|
|
113
111
|
if (!metadata.phases) {
|
|
114
112
|
metadata.phases = {};
|
|
115
113
|
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Static price table for converting per-agent token counters to USD.
|
|
4
|
+
*
|
|
5
|
+
* Issue #330 / R1.3 / C4 (ISO 27001 A.5.31).
|
|
6
|
+
*
|
|
7
|
+
* Each entry MUST carry `source` (a citation pointing back to the vendor's
|
|
8
|
+
* published pricing page) and `verifiedOn` (an ISO date string). The
|
|
9
|
+
* `agent-token-prices.test.ts` lint fails CI if any entry exceeds 180 days
|
|
10
|
+
* since `verifiedOn` so this table is reviewed at least quarterly.
|
|
11
|
+
*
|
|
12
|
+
* Prices are USD per million tokens.
|
|
13
|
+
*/
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.AGENT_TOKEN_PRICES = void 0;
|
|
16
|
+
exports.lookupPrice = lookupPrice;
|
|
17
|
+
exports.computeCostUsd = computeCostUsd;
|
|
18
|
+
exports.AGENT_TOKEN_PRICES = [
|
|
19
|
+
// Anthropic — Claude (used by Claude Code).
|
|
20
|
+
// Note: when Claude Code emits cost via OTLP we read it directly; this
|
|
21
|
+
// table backstops cost computation for legacy/no-cost-metric snapshots
|
|
22
|
+
// and lets per-agent comparison panels report a model-attributed cost
|
|
23
|
+
// even when a snapshot lacks a costUsd field.
|
|
24
|
+
{
|
|
25
|
+
agent: 'claude-code',
|
|
26
|
+
model: 'claude-opus-4-7',
|
|
27
|
+
inputPerMTok: 15.00,
|
|
28
|
+
outputPerMTok: 75.00,
|
|
29
|
+
cacheReadPerMTok: 1.50,
|
|
30
|
+
cacheCreationPerMTok: 18.75,
|
|
31
|
+
source: 'https://www.anthropic.com/pricing',
|
|
32
|
+
verifiedOn: '2026-04-29',
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
agent: 'claude-code',
|
|
36
|
+
model: 'claude-sonnet-4-6',
|
|
37
|
+
inputPerMTok: 3.00,
|
|
38
|
+
outputPerMTok: 15.00,
|
|
39
|
+
cacheReadPerMTok: 0.30,
|
|
40
|
+
cacheCreationPerMTok: 3.75,
|
|
41
|
+
source: 'https://www.anthropic.com/pricing',
|
|
42
|
+
verifiedOn: '2026-04-29',
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
agent: 'claude-code',
|
|
46
|
+
model: 'claude-haiku-4-5',
|
|
47
|
+
inputPerMTok: 1.00,
|
|
48
|
+
outputPerMTok: 5.00,
|
|
49
|
+
cacheReadPerMTok: 0.10,
|
|
50
|
+
cacheCreationPerMTok: 1.25,
|
|
51
|
+
source: 'https://www.anthropic.com/pricing',
|
|
52
|
+
verifiedOn: '2026-04-29',
|
|
53
|
+
},
|
|
54
|
+
// OpenAI — GPT-5 Codex (used by Codex CLI).
|
|
55
|
+
{
|
|
56
|
+
agent: 'codex',
|
|
57
|
+
model: 'gpt-5-codex',
|
|
58
|
+
inputPerMTok: 5.00,
|
|
59
|
+
outputPerMTok: 15.00,
|
|
60
|
+
cacheReadPerMTok: 0.50,
|
|
61
|
+
cacheCreationPerMTok: 0,
|
|
62
|
+
source: 'https://openai.com/api/pricing/',
|
|
63
|
+
verifiedOn: '2026-04-29',
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
agent: 'codex',
|
|
67
|
+
model: 'gpt-5',
|
|
68
|
+
inputPerMTok: 5.00,
|
|
69
|
+
outputPerMTok: 15.00,
|
|
70
|
+
cacheReadPerMTok: 0.50,
|
|
71
|
+
cacheCreationPerMTok: 0,
|
|
72
|
+
source: 'https://openai.com/api/pricing/',
|
|
73
|
+
verifiedOn: '2026-04-29',
|
|
74
|
+
},
|
|
75
|
+
// Real Codex CLI 0.125.0 emits `gpt-5.5` on turn_context.payload.model.
|
|
76
|
+
// Treated as the gpt-5 family for pricing until OpenAI publishes a
|
|
77
|
+
// distinct rate card. Conservative estimate; refresh on next quarterly
|
|
78
|
+
// verification pass.
|
|
79
|
+
{
|
|
80
|
+
agent: 'codex',
|
|
81
|
+
model: 'gpt-5.5',
|
|
82
|
+
inputPerMTok: 5.00,
|
|
83
|
+
outputPerMTok: 15.00,
|
|
84
|
+
cacheReadPerMTok: 0.50,
|
|
85
|
+
cacheCreationPerMTok: 0,
|
|
86
|
+
source: 'https://openai.com/api/pricing/ (gpt-5 row, applied to gpt-5.5 as nearest published rate)',
|
|
87
|
+
verifiedOn: '2026-04-30',
|
|
88
|
+
},
|
|
89
|
+
];
|
|
90
|
+
/**
|
|
91
|
+
* Look up the price entry for an agent + model. Agent is matched
|
|
92
|
+
* case-insensitively. Returns null when no matching entry exists — the
|
|
93
|
+
* adapter is then expected to skip cost computation rather than guess.
|
|
94
|
+
*/
|
|
95
|
+
function lookupPrice(agent, model) {
|
|
96
|
+
if (!agent || !model)
|
|
97
|
+
return null;
|
|
98
|
+
const a = agent.toLowerCase();
|
|
99
|
+
return exports.AGENT_TOKEN_PRICES.find(e => e.agent === a && e.model === model) || null;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Compute USD cost from cumulative token counts. Returns 0 when no price
|
|
103
|
+
* entry exists for the agent/model — the caller decides whether to drop
|
|
104
|
+
* the snapshot entirely or surface a partial-coverage warning.
|
|
105
|
+
*/
|
|
106
|
+
function computeCostUsd(agent, model, counts) {
|
|
107
|
+
const price = lookupPrice(agent, model);
|
|
108
|
+
if (!price)
|
|
109
|
+
return 0;
|
|
110
|
+
return ((counts.inputTokens / 1_000_000) * price.inputPerMTok +
|
|
111
|
+
(counts.outputTokens / 1_000_000) * price.outputPerMTok +
|
|
112
|
+
(counts.cacheReadTokens / 1_000_000) * price.cacheReadPerMTok +
|
|
113
|
+
(counts.cacheCreationTokens / 1_000_000) * price.cacheCreationPerMTok);
|
|
114
|
+
}
|